3分钟生成一个单元测试报告,这个样式爱了
昨天有个小伙伴问我,有没有什么现成的测试报告模板,由于昨天实在比较忙就没顾上,所以今个有时间赶紧补上。一般力所能及的事,只要我有时间都会为大家解决,但毕竟能力有限做不到的地方小伙伴们也多理解。
平时我们开发接口时,Junit 单元测试是最为常用的一种开发测试手段,很多时候测试其实只看接口是否正常返回结果就 ok 了。但有时间我们要测试一些特殊场景,如:接口超时测试等,就没什么太好的办法了,而 TestNG 实现容易的多。它与 JUnit 用法十分相似,只要你用过 JUnit 分分钟上手。
大致讲一下 TestNG 的几个重要概念,@Test 注解标注的方法是最小的执行单元,我们可以将这些单个的测试用例划分成 group 分组管理,group 可以用在测试类或者方法上,suite 套件可以理解成测试类的容器。
下边我们搭建一个TestNG测试框架,结合具体案例介绍一下它的功能。 核心依赖
引入 extentreports 和 testng org.springframework.boot spring-boot-starter-test test org.junit.vintage junit-vintage-engine org.testng testng 7.1.0 test com.aventstack extentreports 3.0.6 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"?> 测试报告配置
手动配置一个测试报告侦听器类 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 xmlSuites, List suites, String outputDirectory) { init(); boolean createSuiteNode = false; if (suites.size() > 1) { createSuiteNode = true; } for (ISuite suite : suites) { Map 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 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 treeSet = new TreeSet(new Comparator() { @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 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 方式下还可以在配置文件设置参数 @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");
终极环保车加速驶上快车道何时能与纯电动并驾齐驱?北京冬奥会期间,816辆氢燃料电池汽车作为主运力开展示范运营服务,是迄今为止在重大国际赛事中投入规模最大的。氢能源汽车也由此进入大众视野。由于在行驶过程中只排放水,氢燃料电池车被业
如何解决高并发秒杀的超卖问题由秒杀引发的一个问题秒杀最大的一个问题就是解决超卖的问题。其中一种解决超卖如下方式1updategoodssetnumnum1WHEREid1001andnum0我们假设现在商品只
我的代码简洁之道1。通过条件判断给变量赋值布尔值的正确姿势badif(aa)btrueelsebfalsegoodbaa2。在if中判断数组长度不为零的正确姿势badif(arr。length!0
努比亚Z40Pro突击,拿下三个首发,实力不容小觑在进入5G时代后不少雪藏已久的老品牌友商都开始爆发,如中兴摩托罗拉努比亚等都再次重出江湖,纷纷拿出很有实力的产品来。但由于当下广大消费者都将目光集中在华为小米OV苹果三星等这些大牌
商业界有哪些功高震主的案例?功高震主在企业界很多,比如华为的李一男,淘宝的孙彤宇,等等。1创立淘宝的孙彤宇孙彤宇是仅次于马云的二号员工,是阿里巴巴的18罗汉之一,还把老婆彭蕾招入了阿里巴巴。现在淘宝的倒立文化
D1芯片伸缩镜头5W隔空充电,这样的配置你爱吗?特斯拉成为新能源领域的一匹野马,其电动汽车畅销全球。特斯拉在自动驾驶方面也取得了很多突破。与此同时,特斯拉创始人埃隆马斯克在2021年底接受采访时曾暗示将推出智能手机。现在特斯拉手
联想杨元庆将启动大规模招聘,三年招12000人鞭牛士2月24日消息,据报道,联想集团董事长杨元庆昨日在接受采访时表示,联想依旧会启动大规模招聘,三年将达到12000人。人员的招聘肯定会坚持下去,我一定会按照承诺去做。据此前消息
这才是玩游戏大作正确方式!谷粒金刚2PRO入手测评作为游戏爱好者,虽然没有SwitchPS5等主机,但是PC端的游戏库也是相当的丰富,为了游戏库里的本地游戏更加充实,我也是升级了1T的固态,至少不用像以前那样,玩通关一个游戏,卸载
几百块和几万块的电视到底有什么区别?原来画质音质等都差那么大本人从事电视行业多年,从客观的角度给大家分析电视的差距(本文不打广告,提到的所有品牌仅供参考)。提到电视,目前市面上品牌非常多,合资品牌如索尼三星LG等,国产品牌海信创维TCL等。
萌新咆哮!C里引用和指针到底有什么区别?我们在写代码的时候总是离不开函数,我们调用一个函数,得到一个想要的结果。这个过程好像自然而然,但有的时候又会遇到一些意想不到的问题让我们困扰。尤其是涉及到函数参数的时候,我们经常会
干货分享单反和微单的真正区别,不仅是反光板五棱镜和取景器现在有不少人对于单反和微单的区别,都还停留在机身的结构上面,很多人认为,单反和微单的区别就是单反机身体积比较大,比较重,主要就是因为单反具有反光板和五棱镜,另外单反有光学取景器,而