3 分钟生成一个单元测试报告,这个样式爱了
昨天有个小伙伴问我,有没有什么现成的测试报告模板,由于昨天实在比较忙就没顾上,所以今个有时间赶紧补上。一般力所能及的事,只要我有时间都会为大家解决,但毕竟能力有限做不到的地方小伙伴们也多理解。
平时我们开发接口时,Junit
单元测试是最为常用的一种开发测试手段,很多时候测试其实只看接口是否正常返回结果就 ok 了。但有时间我们要测试一些特殊场景,如:接口超时测试等,就没什么太好的办法了,而 TestNG
实现容易的多。它与 JUnit
用法十分相似,只要你用过 JUnit
分分钟上手。
大致讲一下 TestNG
的几个重要概念,@Test
注解标注的方法是最小的执行单元,我们可以将这些单个的测试用例划分成 group
分组管理,group
可以用在测试类或者方法上,suite
套件可以理解成测试类的容器。
下边我们搭建一个TestNG
测试框架,结合具体案例介绍一下它的功能。
核心依赖
引入 extentreports 和 testng
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.testng</groupId> <artifactId>testng</artifactId> <version>7.1.0</version> <scope>test</scope> </dependency> <dependency> <groupId>com.aventstack</groupId> <artifactId>extentreports</artifactId> <version>3.0.6</version> </dependency></dependencies>
TestNG 配置
TestNG
支持两种执行方式,第一种是用注解像 Junit
直接点方法名 run
执行。第二种配置 xml
文件的方式。
@Slf4j@Listeners({ExtentTestNGIReporterListener.class})@SpringBootTest(classes = SpringbootTestngReportApplication.class)public class UserTest extends AbstractTestNGSpringContextTests { @Data class User { private Integer userId; private String userName; } /** * 参数提供 */ @DataProvider(name = "paramDataProvider") public Object[][] paramDataProvider() { User user1 = new User(); user1.setUserId(1); user1.setUserName("程序员内点事1"); User user2 = new User(); user2.setUserId(2); user2.setUserName("程序员内点事2"); return new Object[][]{{1, user1}, {2, user2}}; } @Test(dataProvider = "paramDataProvider") public void queryUser(Integer index, User user) { if (index == 2) { int a = 1 / 0; } log.info("index:{},user: {}", index, JSON.toJSONString(user)); Assert.assertTrue(Objects.nonNull(user)); }}
xml
方式直接右键 .xml
文件 run
就运行了。
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd"><suite name="用户单元测试" parallel="classes" thread-count="5"> <listeners> <listener class-name="com.xiaofu.report.config.ExtentTestNGIReporterListener"/> </listeners> <test verbose="1" name="用户测试"> <parameter name="userId" value="1"/> <parameter name="userName" value="程序员内点事"/> <groups> <define name="queryUser"/> <define name="queryUser1"/> </groups> <classes> <class name="com.xiaofu.report.UserTest"/> </classes> </test></suite>
测试报告配置
手动配置一个测试报告侦听器类 ExtentTestNGIReporterListener
,可以自行定义在测试报告上显示的数据,最后执行测试方法同时会生成测试报告。
/** * @author xiaofu * @description TestNg 可视化配置 * @date 2020/3/19 16:44 */public class ExtentTestNGIReporterListener implements IReporter { //生成的路径以及文件名 private static final String OUTPUT_FOLDER = "target/test-report/"; private static final String FILE_NAME = "index.html"; private ExtentReports extent; @Override public void generateReport(List<XmlSuite> xmlSuites, List<ISuite> suites, String outputDirectory) { init(); boolean createSuiteNode = false; if (suites.size() > 1) { createSuiteNode = true; } for (ISuite suite : suites) { Map<String, ISuiteResult> result = suite.getResults(); //如果suite里面没有任何用例,直接跳过,不在报告里生成 if (result.size() == 0) { continue; } //统计suite下的成功、失败、跳过的总用例数 int suiteFailSize = 0; int suitePassSize = 0; int suiteSkipSize = 0; ExtentTest suiteTest = null; //存在多个suite的情况下,在报告中将同一个一个suite的测试结果归为一类,创建一级节点。 if (createSuiteNode) { suiteTest = extent.createTest(suite.getName()).assignCategory(suite.getName()); } boolean createSuiteResultNode = false; if (result.size() > 1) { createSuiteResultNode = true; } for (ISuiteResult r : result.values()) { ExtentTest resultNode; ITestContext context = r.getTestContext(); if (createSuiteResultNode) { //没有创建suite的情况下,将在SuiteResult的创建为一级节点,否则创建为suite的一个子节点。 if (null == suiteTest) { resultNode = extent.createTest(r.getTestContext().getName()); } else { resultNode = suiteTest.createNode(r.getTestContext().getName()); } } else { resultNode = suiteTest; } if (resultNode != null) { resultNode.getModel().setName(suite.getName() + " : " + r.getTestContext().getName()); if (resultNode.getModel().hasCategory()) { resultNode.assignCategory(r.getTestContext().getName()); } else { resultNode.assignCategory(suite.getName(), r.getTestContext().getName()); } resultNode.getModel().setStartTime(r.getTestContext().getStartDate()); resultNode.getModel().setEndTime(r.getTestContext().getEndDate()); //统计SuiteResult下的数据 int passSize = r.getTestContext().getPassedTests().size(); int failSize = r.getTestContext().getFailedTests().size(); int skipSize = r.getTestContext().getSkippedTests().size(); suitePassSize += passSize; suiteFailSize += failSize; suiteSkipSize += skipSize; if (failSize > 0) { resultNode.getModel().setStatus(Status.FAIL); } resultNode.getModel().setDescription(String.format("Pass: %s ; Fail: %s ; Skip: %s ;", passSize, failSize, skipSize)); } buildTestNodes(resultNode, context.getFailedTests(), Status.FAIL); buildTestNodes(resultNode, context.getSkippedTests(), Status.SKIP); buildTestNodes(resultNode, context.getPassedTests(), Status.PASS); } if (suiteTest != null) { suiteTest.getModel().setDescription(String.format("Pass: %s ; Fail: %s ; Skip: %s ;", suitePassSize, suiteFailSize, suiteSkipSize)); if (suiteFailSize > 0) { suiteTest.getModel().setStatus(Status.FAIL); } } } for (String s : Reporter.getOutput()) { extent.setTestRunnerOutput(s); } extent.flush(); } private void init() { //文件夹不存在的话进行创建 File reportDir = new File(OUTPUT_FOLDER); if (!reportDir.exists() && !reportDir.isDirectory()) { reportDir.mkdirs(); } ExtentHtmlReporter htmlReporter = new ExtentHtmlReporter(OUTPUT_FOLDER + FILE_NAME); // 设置静态文件的DNS //怎么样解决cdn.rawgit.com访问不了的情况 htmlReporter.config().setResourceCDN(ResourceCDN.EXTENTREPORTS); htmlReporter.config().setDocumentTitle("用户服务自动化测试报告"); htmlReporter.config().setReportName("用户服务自动化测试报告"); htmlReporter.config().setChartVisibilityOnOpen(true); htmlReporter.config().setTestViewChartLocation(ChartLocation.TOP); htmlReporter.config().setTheme(Theme.STANDARD); htmlReporter.config().setEncoding("utf-8"); htmlReporter.config().setCSS(".node.level-1 ul{ display:none;} .node.level-1.active ul{display:block;}"); extent = new ExtentReports(); extent.attachReporter(htmlReporter); extent.setReportUsesManualConfiguration(true); } private void buildTestNodes(ExtentTest extenttest, IResultMap tests, Status status) { //存在父节点时,获取父节点的标签 String[] categories = new String[0]; if (extenttest != null) { List<TestAttribute> categoryList = extenttest.getModel().getCategoryContext().getAll(); categories = new String[categoryList.size()]; for (int index = 0; index < categoryList.size(); index++) { categories[index] = categoryList.get(index).getName(); } } ExtentTest test; if (tests.size() > 0) { //调整用例排序,按时间排序 Set<ITestResult> treeSet = new TreeSet<ITestResult>(new Comparator<ITestResult>() { @Override public int compare(ITestResult o1, ITestResult o2) { return o1.getStartMillis() < o2.getStartMillis() ? -1 : 1; } }); treeSet.addAll(tests.getAllResults()); for (ITestResult result : treeSet) { Object[] parameters = result.getParameters(); String name = ""; //如果有参数,则使用参数的toString组合代替报告中的name for (Object param : parameters) { name += param.toString(); } if (name.length() == 0) { name = result.getMethod().getMethodName(); } if (extenttest == null) { test = extent.createTest(name); } else { //作为子节点进行创建时,设置同父节点的标签一致,便于报告检索。 test = extenttest.createNode(name).assignCategory(categories); } //test.getModel().setDescription(description.toString()); //test = extent.createTest(result.getMethod().getMethodName()); for (String group : result.getMethod().getGroups()) test.assignCategory(group); List<String> outputList = Reporter.getOutput(result); for (String output : outputList) { //将用例的log输出报告中 test.debug(output); } if (result.getThrowable() != null) { test.log(status, result.getThrowable()); } else { test.log(status, "Test " + status.toString().toLowerCase() + "ed"); } test.getModel().setStartTime(getTime(result.getStartMillis())); test.getModel().setEndTime(getTime(result.getEndMillis())); } } } private Date getTime(long millis) { Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(millis); return calendar.getTime(); }}
会在指定的目录 target/test-report/
下生成 index.html
测试报告文件,测试的成功率等信息显示的都比较直观,样式也还是蛮好看。
测试场景
下边就简单介绍几个我常用的 testNG 测试场景
1、参数化测试
使用 @DataProvider
注解为其他测试方法提供参数,queryUser
方法会执行 Object[][]
数组中所有参数user1 、user2,相当于循环执行测试方法。
@DataProvider(name = "paramDataProvider")public Object[][] paramDataProvider() { User user1 = new User(); user1.setUserId(1); user1.setUserName("程序员内点事1"); User user2 = new User(); user2.setUserId(2); user2.setUserName("程序员内点事2"); return new Object[][]{{1, user1}, {2, user2}};}@Test(dataProvider = "paramDataProvider",groups = "user")public void queryUser(Integer index, User user) { log.info("index:{},user: {}", index, JSON.toJSONString(user));}
xml 方式下还可以在配置文件设置参数
<parameter name="name" value="程序员内点事"/>
@Test(groups = "user")public void queryUser(String name) { log.info("我是测试方法~");}
2、超时测试
可以给测试方法一个超时时间,如果实际执行时间超过设定的超时时间,用例将不通过。
@Test(timeOut = 5000)public void timeOutTest() throws InterruptedException { Thread.sleep(6000);}
3、依赖测试
有时我们可能需要以特定顺序调用测试用例中的方法,或者希望在方法之间共享一些数据,TestNG
支持在测试方法之间显式依赖的声明。
@Test public void token() { System.out.println("get token"); } @Test(dependsOnMethods= {"token"}) public void getUser() { System.out.println("this is test getUser"); }
总结
简单提了一下 TestNG
框架相关的知识,说实话本来就为给老铁弄个测试报告模板,一不留神说这么多。如果小伙伴们对这个测试框架感兴趣,下次我会出一份详细的 TestNG
文章。
原创不易,燃烧秀发输出内容,如果有一丢丢收获,点个赞鼓励一下吧!
整理了几百本各类技术电子书,送给小伙伴们。关注公号回复【666】自行领取。和一些小伙伴们建了一个技术交流群,一起探讨技术、分享技术资料,旨在共同学习进步,如果感兴趣就加入我们吧!
版权声明: 本文为 InfoQ 作者【程序员内点事】的原创文章。
原文链接:【http://xie.infoq.cn/article/6be58cee2ab8f8f30ff0b3f7b】。文章转载请联系作者。
程序员内点事
不积跬步,无以至千里 2018.07.03 加入
公众号-程序员内点事,一个技术传播者
评论