最近接触了一个基于cucumber开发的测试框架,感觉挺不错的。想把整个框架的设计以及实现具体记录下,已方便自己后面的查询和参考。
想想主要从以下几个方面来阐述:
一、框架基本介绍
框架展示形式基于cucumber,对于cucumber的使用不再多说可以百度出很多介绍,该框架基于cucumber+spring+testng。下面就基于修改地方简单描述:
1、本框架基于cucumber可以灵活操作变量,将变量级别分为 全局变量、feature级变量,测试人员可以自定义变量,也可以在关键字中灵活的保存为对应级别的变量已方便后面的操作。
2、运行多套环境,多套数据。只需要配置对应的环境配置。例如 测试环境 配置为qa 联调环境配置为int,即可完成环境之间的切换(FileHandle.FILE_EXT)
3、封装多个关键字。可以直接使用 如 http请求 dubbo 请求 操作数据库的关键字....(这个不是重点,可以自己不断的完善补充)
5、集成spring,可以方便的使用spring的依赖注入(这个也不是重点,cucumber集成spring的包cucumber-spring。需要特别注意的是spring的配置文件名称必须为cucumber.xml)
4、清晰完善的测试报告(这个也不是重点,cucumber测试报告有很多插件..cucumber-reporting-2.2.0.jar)
二、框架的设计思路以及代码实现
如下图为针对cucumber主要改造的设计图
运行的用例集成cucumber-testng的抽象方法AbstractTestNGCucumberTests,该抽象方法主要有四个方法,上面的图中缺少一个dataProvider方法,主要是运行过程中数据提供者,上图中的三个方法非常明了:
1、BeforeClass 运行前的初始化 在这个步骤中假如我们自己的全局变量以及一些初始化工作TestContext。
2、feature 运行对应feature,数据由dataProvide提供。在这个步骤中针对每个feature加入feature变量hookUtil.beforeFeature(cucumberFeature);
3、根据方法配置的测试报告路径,生成对应的测试报告
先上下上面描述的代码:
1、运行测试的类
@Test @CucumberOptions(features={"src/main/resources/feature/dubbotest/demo.feature"},glue={"com.tom.test"},plugin = {"pretty", "html:./target/cucumber","json:./target/cucumber/report.json"},tags={}) public class SingleTest extends SpecTestNgCuke { }
2、SpecTestNgCuke类===等价AbstractTestNGCucumberTests import java.io.File; import java.util.ArrayList; import java.util.List; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import com.tom.utils.HookUtil; import com.tom.utils.TestContext; import cucumber.api.CucumberOptions; import cucumber.api.testng.CucumberFeatureWrapper; import net.masterthought.cucumber.Configuration; import net.masterthought.cucumber.ReportBuilder; public class SpecTestNgCuke { private SpecCucumberRunner testNGCucumberRunner; private File jsonPath=null; @BeforeClass(alwaysRun = true) public void setUpClass() throws Exception { TestContext.getInstance().init(); Class<? extends SpecTestNgCuke> clazz=this.getClass(); testNGCucumberRunner = new SpecCucumberRunner(clazz); //拿注解 CucumberOptions options=clazz.getAnnotation(CucumberOptions.class); for(String plugin:options.plugin()){ //判断是json if(plugin.trim().startsWith("json")){ jsonPath=new File(plugin.split(":")[1]); } } } @Test(groups = "cucumber", description = "Runs Cucumber Feature", dataProvider = "features") public void feature(CucumberFeatureWrapper cucumberFeature) { HookUtil hookUtil = new HookUtil(); hookUtil.beforeFeature(cucumberFeature); try { testNGCucumberRunner.runCucumber(cucumberFeature.getCucumberFeature()); } catch (Exception e) { // TODO Auto-generated catch block throw e; } finally { hookUtil.afterFeature(cucumberFeature); } } /** * @return returns two dimensional array of {@link CucumberFeatureWrapper} * objects. */ @DataProvider public Object[][] features() { return testNGCucumberRunner.provideFeatures(); } @AfterClass public void tearDownClass() throws Exception { testNGCucumberRunner.finish(); //生成报告 File reportOutputDirectory = new File("target/cucumber"); List<String> jsonFiles = new ArrayList<>(); jsonFiles.add(jsonPath.getAbsolutePath()); String projectName = "cuke"; Configuration configuration = new Configuration(reportOutputDirectory, projectName); ReportBuilder reportBuilder = new ReportBuilder(jsonFiles, configuration); reportBuilder.generateReports(); } }
3、重要核心类 TestContext 片段,该类为单例。
public class TestContext { private static Logger logger = (Logger) LoggerFactory.getLogger(TestContext.class); private static TestContext testContext = null; // private boolean internalScenario = false; Map<String, Object> scenarioParas = new HashMap<>(); //定义变量保存在一个MAP中 Map<String, Object> featureParas = new HashMap<>(); //定义变量保存在一个MAP中 Map<String, Object> globalParas = new HashMap<>(); //定义变量保存在一个MAP中 public final String LAST_RESPONSE_STRING = "last_response_string"; private DubboHandle dubboHandle; private FileHandle fileHandle; private ExceptionHandle exceptionHandle; private MQProducer mqProducer; private OutputHandle outputHandle; private RSAHandle rsaHandle; private TestContext() { } public void init() throws Exception { dubboHandle = new DubboHandle(); fileHandle = new FileHandle(); exceptionHandle = new ExceptionHandle(); rsaHandle=new RSAHandle(); fileHandle.init(); dubboHandle.init(fileHandle.getFileExt()); this.outputHandle = new OutputHandle(); setMqProducer(new MQProducer()); rsaHandle.init(); // 初始化全局参数 try { putGlobalParameters(fileHandle.loadParaFile(Thread.currentThread().getContextClassLoader() .getResource("global/common." + fileHandle.getFileExt()).getPath())); } catch (Exception e) { // TODO Auto-generated catch block logger.warn("全局参数文件,gloal/common" + fileHandle.getFileExt()+ "不存在"); } } /** * 获取实例 * * @return * @throws Exception */ public static TestContext getInstance() { if (testContext == null) { try { testContext = new TestContext(); } catch (Exception e) { // TODO Auto-generated catch block logger.error("没有找到 全局变量文件 \n" + e.getMessage()); } } return testContext; }
4、通过java关键字解析是否变量是否在各个级别中
/** * 将字符串中含有 ${}或 $()的替换成值,分三种情况,一种是${}完全的,一种是在中间,还有$()这种,统一变成 json字符串 * * @param toParse * @return 对象返回json格式,简单类型直接返回 */ public String parseParameter(String toParse) { String result = toParse; Pattern pattern = Pattern.compile("\\$\\{(.+?)\\}"); Matcher matcher = pattern.matcher(toParse); while (matcher.find()) { String para = matcher.group(); // 判断方法 String pname = matcher.group(1); String value = null; if (pname.contains("(") && pname.contains(")")) { Pattern methodPattern = Pattern.compile("(.+?)\\((.*?)\\)"); Matcher methodMatcher = methodPattern.matcher(pname.trim()); if (methodMatcher.find()) { // 先不考虑传参数的情况, String methodName = methodMatcher.group(1); String orValues=methodMatcher.group(2).trim(); String[] keyValues=orValues.split("\\|"); if(keyValues[0].isEmpty()){ keyValues=new String[0]; } String className = null; Class clazz = null; try { if (!methodName.contains(".")) { clazz = FunctionUtil.class; } else { // 包名 com.tom.test.Function.test() className = methodName.substring(0, methodName.lastIndexOf(".")); clazz = Class.forName(className); } methodName = methodName.substring(methodName.lastIndexOf(".") + 1); List<Class> types=new ArrayList<>(); List<Object> values=new ArrayList<>(); for(int i=0;i<keyValues.length;i++){ String[] kv=keyValues[i].split("="); types.add(ClassUtils._forName(kv[0])); values.add(JSON.parse(kv[1], ClassUtils._forName(kv[0]))); } Method method = clazz.getMethod(methodName, types.toArray(new Class[types.size()])); value=method.invoke(clazz.newInstance(), values.toArray()).toString(); } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (ClassNotFoundException e) { throw new RuntimeException("方法:" + pname + " 无法找到"); } catch (InstantiationException e) { throw new RuntimeException("类:" + className + " 构造失败"); } catch (ParseException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } else { Object obj = TestContext.getInstance().getParameter(para); if (obj instanceof String) { value = obj.toString(); } else { value = new JsonUtil().objectToString(obj); } } result = result.replace(para, value); }
四、依赖的jar包以及源码地址
参考依赖的jar包
<dependency> <groupId>info.cukes</groupId> <artifactId>cucumber-java</artifactId> <version>1.2.4</version> </dependency> <dependency> <groupId>info.cukes</groupId> <artifactId>cucumber-testng</artifactId> <version>1.2.4</version> </dependency> <dependency> <groupId>org.testng</groupId> <artifactId>testng</artifactId> <version>6.8.17</version> </dependency>
<dependency> <groupId>info.cukes</groupId> <artifactId>cucumber-spring</artifactId> <version>1.2.4</version> </dependency>
源码git地址:
https://github.com/simple365/Pineapple