• TestNG与ExtentReport单元测试导出报告文档


    TestNG与ExtentReport集成

    目录

    1 通过实现ITestListener的方法添加Reporter log
      1.1 MyTestListener设置
      1.2 输出结果
    2 TestNG与ExtentReporter集成
      2.1 项目结构
      2.2 MyExtentReportListener设置
      2.3 单多Suite、Test组合测试
        2.3.1 单Suite单Test
        2.3.2 单Suite多Test
        2.3.3 多Suite

    源代码:interface-test-framework.zip

    1 通过实现ITestListener的方法添加Reporter log

    TestNG的Listener列表

    TestNG提供了一组预定义的Listener Java接口,这些接口全部继承自TestNG的 ITestNGListener接口。用户创建这些接口的实现类,并把它们加入到 TestNG 中,TestNG便会在测试运行的不同时刻调用这些类中的接口方法:

    • IExecutionListener   监听TestNG运行的启动和停止。
    • IAnnotationTransformer 注解转换器,用于TestNG测试类中的注解。
    • ISuiteListener 测试套件监听器,监听测试套件的启动和停止。
    • ITestListener  测试方法执行监听。
    • IConfigurationListener 监听配置方法相关的接口。
    • IMethodInterceptor 拦截器,调整测试方法的执行顺序。
    • IInvokedMethodListener 测试方法拦截监听,用于获取被TestNG调用的在Method的Before 和After方法监听器。该方法只会被配置和测试方法调用。
    • IHookable 若测试类实现了该接口,当@Test方法被发现时,它的run()方法将会被调用来替代@Test方法。这个测试方法通常在IHookCallBack的callback之上调用,比较适用于需要JASS授权的测试类。
    • IReporter 实现该接口可以生成一份测试报告。

    本文将着重介绍最常用到的两个Listener ITestListener与Ireporter接口。

    1.1 MyTestListener设置

    通过实现ITestListener的方法,添加Reporter.log

    MyTestListener.java

    import org.testng.ITestContext;
    import org.testng.ITestListener;
    import org.testng.ITestResult;
    import org.testng.Reporter;
    
    public class MyTestListener implements ITestListener {
        //用例执行结束后,用例执行成功时调用
        public void onTestSuccess(ITestResult tr) {
            logTestEnd(tr, "Success");
        }
    
        //用例执行结束后,用例执行失败时调用
        public void onTestFailure(ITestResult tr) {
            logTestEnd(tr, "Failed");
        }
    
        //用例执行结束后,用例执行skip时调用
        public void onTestSkipped(ITestResult tr) {
            logTestEnd(tr, "Skipped");
        }
    
        //每次方法失败但是已经使用successPercentage进行注释时调用,并且此失败仍保留在请求的成功百分比之内。
        public void onTestFailedButWithinSuccessPercentage(ITestResult tr) {
            logTestEnd(tr, "FailedButWithinSuccessPercentage");
        }
    
        //每次调用测试@Test之前调用
        public void onTestStart(ITestResult result) {
            logTestStart(result);
        }
    
        //在测试类被实例化之后调用,并在调用任何配置方法之前调用。
        public void onStart(ITestContext context) {
            return;
        }
    
        //在所有测试运行之后调用,并且所有的配置方法都被调用
        public void onFinish(ITestContext context) {
            return;
        }
    
        // 在用例执行结束时,打印用例的执行结果信息
        protected void logTestEnd(ITestResult tr, String result) {
            Reporter.log(String.format("-------------Result: %s-------------", result), true);
        }
    
        // 在用例开始时,打印用例的一些信息,比如@Test对应的方法名,用例的描述等等
        protected void logTestStart(ITestResult tr) {
            Reporter.log(String.format("-------------Run: %s.%s---------------", tr.getTestClass().getName(), tr.getMethod().getMethodName()), true);
            Reporter.log(String.format("用例描述: %s, 优先级: %s", tr.getMethod().getDescription(), tr.getMethod().getPriority()),true);
            return;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    1.2 输出结果

    SmkDemo1.java

    import com.demo.listener.MyTestListener;
    import org.testng.Assert;
    import org.testng.Reporter;
    import org.testng.annotations.Listeners;
    import org.testng.annotations.Test;
    
    @Listeners({MyTestListener.class})
    public class SmkDemo1 {
        @Test(description="测testPass11的描述",priority = 1,groups = {"分组1"})
        public void testPass11(){
            Reporter.log("Test11的第一步",true);
            Assert.assertEquals(1,1);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    输出界面显示如下

    图1 log to 输出界面

    2 TestNG与ExtentReporter集成

    2.1 项目结构

    图2 项目结构

    pom.xml

    
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0modelVersion>
    
        <groupId>com.demogroupId>
        <artifactId>interface-test-frameworkartifactId>
        <version>1.0-SNAPSHOTversion>
    
        <dependencies>
            
            <dependency>
                <groupId>com.vimalselvamgroupId>
                <artifactId>testng-extentsreportartifactId>
                <version>1.3.1version>
            dependency>
            
            <dependency>
                <groupId>com.aventstackgroupId>
                <artifactId>extentreportsartifactId>
                <version>3.0.6version>
            dependency>
            <dependency>
                <groupId>org.testnggroupId>
                <artifactId>testngartifactId>
                <version>7.1.0version>
            dependency>
        dependencies>
    project>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    2.2 MyExtentReportListener设置

    MyExtentReporterListener.java

    import com.aventstack.extentreports.ExtentReports;
    import com.aventstack.extentreports.ExtentTest;
    import com.aventstack.extentreports.ResourceCDN;
    import com.aventstack.extentreports.Status;
    import com.aventstack.extentreports.reporter.ExtentHtmlReporter;
    import com.aventstack.extentreports.reporter.configuration.ChartLocation;
    import com.aventstack.extentreports.reporter.configuration.Theme;
    import org.testng.*;
    import org.testng.xml.XmlSuite;
    
    import java.io.File;
    import java.util.*;
    
    public class MyExtentReporterListener implements IReporter {
        //生成的路径以及文件名
        private static final String OUTPUT_FOLDER = "test-output/";
        private static final String FILE_NAME = "report.html";
    
        private ExtentReports extent;
    
        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());
                    suiteTest = extent.createTest(suite.getName());
                }
                boolean createSuiteResultNode = false;
                if(result.size()>1){
                    createSuiteResultNode=true;
                }
                for (ISuiteResult r : result.values()) {
                    ExtentTest resultNode=null;
                    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;
                    }
                    String[] categories=new String[1];
                    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());
                            categories[0]=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,categories,context.getFailedTests(), Status.FAIL);
                    buildTestNodes(resultNode,categories,context.getSkippedTests(), Status.SKIP);
                    buildTestNodes(resultNode,categories,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.mkdir();
            }
            ExtentHtmlReporter htmlReporter = new ExtentHtmlReporter(OUTPUT_FOLDER + FILE_NAME);
            // 设置静态文件的DNS
            //解决cdn访问不了的问题
            htmlReporter.config().setResourceCDN(ResourceCDN.EXTENTREPORTS);
    
            htmlReporter.config().setDocumentTitle("api自动化测试报告");
            htmlReporter.config().setReportName("api自动化测试报告");
            htmlReporter.config().setChartVisibilityOnOpen(true);
            htmlReporter.config().setTestViewChartLocation(ChartLocation.TOP);
            htmlReporter.config().setTheme(Theme.STANDARD);
            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, String[] categories, 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
    //                categories[index] = categoryList.get(index).getName();
    //            }
    //        }
    
            ExtentTest test;
    
            if (tests.size() > 0) {
                //调整用例排序,按时间排序
                Set<ITestResult> treeSet = new TreeSet<ITestResult>(new Comparator<ITestResult>() {
                    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){
                        if(name.length()>50){
                            name= name.substring(0,49)+"...";
                        }
                    }else{
                        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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195

    2.3 单多Suite、Test组合测试

    2.3.1 单Suite单Test

    testngSingleSuiteSingleTest.xml

    DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
    <suite name="SingleSuite">
        <test name="SingleTest" verbose="1" preserve-order="true" >
            <classes>
                <class name="com.demo.testcase.smoke.SmkDemo1">
                class>
                <class name="com.demo.testcase.sit.SitDemo2">
                class>
            classes>
        test>
        
        <listeners>
            <listener class-name="com.demo.listener.MyTestListener"/>
            <listener class-name="com.demo.listener.MyExtentReporterListener"/>
        listeners>
    suite>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    图3 单Suite单Test总览

    图4 单Suite单Test分组

    图5 单Suite单Test错误分组

    图6 单Suite单Test Dashboard

    2.3.2 单Suite多Test

    testngSingleSuiteDoubleTest.xml

    DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
    <suite name="SingleSuite">
        <test name="DoubleTest1" verbose="1" preserve-order="true" >
            <classes>
                <class name="com.demo.testcase.smoke.SmkDemo1">
                class>
            classes>
        test>
        <test name="DoubleTest2" verbose="1" preserve-order="true" >
            <classes>
                <class name="com.demo.testcase.sit.SitDemo2">
                class>
            classes>
        test>
        
        <listeners>
            <listener class-name="com.demo.listener.MyTestListener"/>
            <listener class-name="com.demo.listener.MyExtentReporterListener"/>
        listeners>
    suite>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    图7 单Suite多Test总览

    图8 单Suite多Test分组

    2.3.3 多Suite

    testngDoubleSuite.xml

    DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
    <suite name="DoubleSuite">
        <suite-files>
            <suite-file path="testngSingleSuiteSingleTest.xml"/>
            <suite-file path="testngSingleSuiteDoubleTest.xml"/>
        suite-files>
        
        <listeners>
            <listener class-name="com.demo.listener.MyTestListener"/>
            <listener class-name="com.demo.listener.MyExtentReporterListener"/>
        listeners>
    suite>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    图9 多Suite总览1

    图10 多Suite总览2

    图11 多Suite分组

    参考

    [1] testng框架Listener介绍及测试结果的收集

    [2] TestNG执行的日志ITestListener与结果IReporter

    [3] TestNG执行的日志ITestListener与结果IReporter

    [4] TestNg Beginner’s Guide–阅后总结之Textng.xml

    [5] TestNg Beginner’s Guide–阅后总结之TestNg注解

    ExtentReports 另一种方法

    引言

    走进Java接口测试之测试框架TestNG 中我们详细介绍了 TestNG 的各种用法, 在本文中,我将详细介绍如何将 ExtentReports 测试报告与TestNG集成。

    ExtentReports 简介

    主要特点:

    • 生成的报告简洁美观
    • 生成的单html方便 Jenkins 集成发邮件
    • 自带集中展示历史报告的服务端
    • 支持 Java 和 .Net

    TestNG 原生报告有点丑,信息整理有点乱。ExtentReports 是用于替换TestNG 原生报告。当然也可以使用 ReportNg,个人偏好 ExtentReports 样式。

    官网已经给了很多demo了,大家可以参考练习。

    官网:http://extentreports.com/

    客户端:

    https://github.com/anshooarora/extentreports-java/commits/master

    服务端:https://github.com/anshooarora/extentx

    Step-1:添加 Maven 依赖包

    引入pom.xml

    
            <dependency>
                <groupId>com.aventstackgroupId>
                <artifactId>extentreportsartifactId>
                <version>3.1.5version>
                <scope>providedscope>
            dependency>
            <dependency>
                <groupId>com.vimalselvamgroupId>
                <artifactId>testng-extentsreportartifactId>
                <version>1.3.1version>
            dependency>
            <dependency>
                <groupId>com.relevantcodesgroupId>
                <artifactId>extentreportsartifactId>
                <version>2.41.2version>
            dependency>
            
            <dependency>
                <groupId>org.testnggroupId>
                <artifactId>testngartifactId>
                <version>6.14.3version>
                <scope>compilescope>
            dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    Step-2:重写 ExtentTestNgFormatter 类

    主要基于以下两项原因:

    • 支持报告中展示更多状态类型的测试结果,例如:成功、失败、警告、跳过等。
    • 因为不支持cdn.rawgit.com访问,故替css访问方式。
    创建 MyExtentTestNgFormatter 类

    下载 ExtentReportes 源码,找到 ExtentTestNgFormatter 类,Listener 目录下创建 MyExtentTestNgFormatter.java 类直接继承 ExtentTestNgFormatter 类。

    public class MyExtentTestNgFormatter extends ExtentTestNgFormatter {
    
    • 1
    解决CDN无法访问

    构造方法加入

    htmlReporter.config().setResourceCDN(ResourceCDN.EXTENTREPORTS);
    
    • 1

    具体代码如下:

    public MyExtentTestNgFormatter() {
          setInstance(this);
          testRunnerOutput = new ArrayList<>();
          String reportPathStr = System.getProperty("reportPath");
          File reportPath;
    
          try {
              reportPath = new File(reportPathStr);
          } catch (NullPointerException e) {
              reportPath = new File(TestNG.DEFAULT_OUTPUTDIR);
          }
    
          if (!reportPath.exists()) {
              if (!reportPath.mkdirs()) {
                  throw new RuntimeException("Failed to create output run directory");
              }
          }
    
          File reportFile = new File(reportPath, "report.html");
          File emailReportFile = new File(reportPath, "emailable-report.html");
    
          htmlReporter = new ExtentHtmlReporter(reportFile);
          EmailReporter emailReporter = new EmailReporter(emailReportFile);
          reporter = new ExtentReports();
          //        如果cdn.rawgit.com访问不了,可以设置为:ResourceCDN.EXTENTREPORTS或者ResourceCDN.GITHUB
          htmlReporter.config().setResourceCDN(ResourceCDN.EXTENTREPORTS);
          reporter.attachReporter(htmlReporter, emailReporter);
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    重写 onstart 方法

    新建一个类名为MyReporter,一个静态ExtentTest的引用。

    Listener 包下 MyReporter.java

    public class MyReporter { public static ExtentTest report; }
    
    • 1

    MyExtentTestNgFormatter.java

    public void onStart(ITestContext iTestContext) {
            ISuite iSuite = iTestContext.getSuite();
            ExtentTest suite = (ExtentTest) iSuite.getAttribute(SUITE_ATTR);
            ExtentTest testContext = suite.createNode(iTestContext.getName());
            // 将MyReporter.report静态引用赋值为testContext。
            // testContext是@Test每个测试用例时需要的。report.log可以跟随具体的测试用例。另请查阅源码。
            MyReporter.report = testContext;
            iTestContext.setAttribute("testContext", testContext);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    自定义配置

    测试报告默认是在工程根目录下创建 test-output/ 文件夹下,名为 report.htmlemailable-report.html。可根据各自需求在构造方法中修改。

    public MyExtentTestNgFormatter() {
            setInstance(this);
            testRunnerOutput = new ArrayList<>();
            // reportPath报告路径
            String reportPathStr = System.getProperty("reportPath");
            File reportPath;
    
            try {
                reportPath = new File(reportPathStr);
            } catch (NullPointerException e) {
                reportPath = new File(TestNG.DEFAULT_OUTPUTDIR);
            }
    
            if (!reportPath.exists()) {
                if (!reportPath.mkdirs()) {
                    throw new RuntimeException("Failed to create output run directory");
                }
            }
            // 报告名report.html
            File reportFile = new File(reportPath, "report.html");
            // 邮件报告名emailable-report.html
            File emailReportFile = new File(reportPath, "emailable-report.html");
    
            htmlReporter = new ExtentHtmlReporter(reportFile);
            EmailReporter emailReporter = new EmailReporter(emailReportFile);
            reporter = new ExtentReports();
            reporter.attachReporter(htmlReporter, emailReporter);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    report.log

    report.log 支持多种玩法

    // 根据状态不同添加报告。型如警告 MyReporter.report.log(Status.WARNING, "接口耗时(ms):" + String.valueOf(time));
    
    • 1

    直接从TestClass 中运行时会报 MyReporter.report 的空指针错误,需做判空处理。

    完整代码
    package com.ruoyi.listener;
    
    import com.aventstack.extentreports.ExtentReports;
    import com.aventstack.extentreports.ExtentTest;
    import com.aventstack.extentreports.ResourceCDN;
    import com.aventstack.extentreports.reporter.ExtentHtmlReporter;
    import com.google.common.base.Preconditions;
    import com.google.common.base.Strings;
    import com.vimalselvam.testng.EmailReporter;
    import com.vimalselvam.testng.NodeName;
    import com.vimalselvam.testng.SystemInfo;
    import com.vimalselvam.testng.listener.ExtentTestNgFormatter;
    import org.testng.*;
    import org.testng.xml.XmlSuite;
    
    import java.io.File;
    import java.io.IOException;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Map;
    
    public class MyExtentTestNgFormatter extends ExtentTestNgFormatter {
        private static final String REPORTER_ATTR = "extentTestNgReporter";
        private static final String SUITE_ATTR = "extentTestNgSuite";
        private ExtentReports reporter;
        private List<String> testRunnerOutput;
        private Map<String, String> systemInfo;
        private ExtentHtmlReporter htmlReporter;
    
        private static ExtentTestNgFormatter instance;
    
        public MyExtentTestNgFormatter() {
            setInstance(this);
            testRunnerOutput = new ArrayList<>();
            // reportPath 报告路径
            String reportPathStr = System.getProperty("reportPath");
            File reportPath;
    
            try {
                reportPath = new File(reportPathStr);
            } catch (NullPointerException e) {
                reportPath = new File(TestNG.DEFAULT_OUTPUTDIR);
            }
    
            if (!reportPath.exists()) {
                if (!reportPath.mkdirs()) {
                    throw new RuntimeException("Failed to create output run directory");
                }
            }
            //  报告名report.html
            File reportFile = new File(reportPath, "report.html");
    		// 邮件报告名emailable-report.html
            File emailReportFile = new File(reportPath, "emailable-report.html");
    
            htmlReporter = new ExtentHtmlReporter(reportFile);
            EmailReporter emailReporter = new EmailReporter(emailReportFile);
            reporter = new ExtentReports();
    	//  如果cdn.rawgit.com访问不了,可以设置为:ResourceCDN.EXTENTREPORTS或者ResourceCDN.GITHUB
            htmlReporter.config().setResourceCDN(ResourceCDN.EXTENTREPORTS);
            reporter.attachReporter(htmlReporter, emailReporter);
        }
    
        /**
         * Gets the instance of the {@link ExtentTestNgFormatter}
         *
         * @return The instance of the {@link ExtentTestNgFormatter}
         */
        public static ExtentTestNgFormatter getInstance() {
            return instance;
        }
    
        private static void setInstance(ExtentTestNgFormatter formatter) {
            instance = formatter;
        }
    
        /**
         * Gets the system information map
         *
         * @return The system information map
         */
        public Map<String, String> getSystemInfo() {
            return systemInfo;
        }
    
        /**
         * Sets the system information
         *
         * @param systemInfo The system information map
         */
        public void setSystemInfo(Map<String, String> systemInfo) {
            this.systemInfo = systemInfo;
        }
    
        public void onStart(ISuite iSuite) {
            if (iSuite.getXmlSuite().getTests().size() > 0) {
                ExtentTest suite = reporter.createTest(iSuite.getName());
                String configFile = iSuite.getParameter("report.config");
    
                if (!Strings.isNullOrEmpty(configFile)) {
                    htmlReporter.loadXMLConfig(configFile);
                }
    
                String systemInfoCustomImplName = iSuite.getParameter("system.info");
                if (!Strings.isNullOrEmpty(systemInfoCustomImplName)) {
                    generateSystemInfo(systemInfoCustomImplName);
                }
    
                iSuite.setAttribute(REPORTER_ATTR, reporter);
                iSuite.setAttribute(SUITE_ATTR, suite);
            }
        }
    
        private void generateSystemInfo(String systemInfoCustomImplName) {
            try {
                Class<?> systemInfoCustomImplClazz = Class.forName(systemInfoCustomImplName);
                if (!SystemInfo.class.isAssignableFrom(systemInfoCustomImplClazz)) {
                    throw new IllegalArgumentException("The given system.info class name <" + systemInfoCustomImplName +
                            "> should implement the interface <" + SystemInfo.class.getName() + ">");
                }
    
                SystemInfo t = (SystemInfo) systemInfoCustomImplClazz.newInstance();
                setSystemInfo(t.getSystemInfo());
            } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
                throw new IllegalStateException(e);
            }
        }
    
        public void onFinish(ISuite iSuite) {
        }
    
    
        public void onTestStart(ITestResult iTestResult) {
            MyReporter.setTestName(iTestResult.getName());
        }
    
        public void onTestSuccess(ITestResult iTestResult) {
    
        }
    
        public void onTestFailure(ITestResult iTestResult) {
    
        }
    
        public void onTestSkipped(ITestResult iTestResult) {
    
        }
    
        public void onTestFailedButWithinSuccessPercentage(ITestResult iTestResult) {
    
        }
    
        public void onStart(ITestContext iTestContext) {
            ISuite iSuite = iTestContext.getSuite();
            ExtentTest suite = (ExtentTest) iSuite.getAttribute(SUITE_ATTR);
            ExtentTest testContext = suite.createNode(iTestContext.getName());
    		// 自定义报告
    		// 将MyReporter.report 静态引用赋值为 testContext。
    		// testContext 是 @Test每个测试用例时需要的。report.log可以跟随具体的测试用例。另请查阅源码。
            MyReporter.report = testContext;
            iTestContext.setAttribute("testContext", testContext);
        }
    
        public void onFinish(ITestContext iTestContext) {
            ExtentTest testContext = (ExtentTest) iTestContext.getAttribute("testContext");
            if (iTestContext.getFailedTests().size() > 0) {
                testContext.fail("Failed");
            } else if (iTestContext.getSkippedTests().size() > 0) {
                testContext.skip("Skipped");
            } else {
                testContext.pass("Passed");
            }
        }
    
        public void beforeInvocation(IInvokedMethod iInvokedMethod, ITestResult iTestResult) {
            if (iInvokedMethod.isTestMethod()) {
                ITestContext iTestContext = iTestResult.getTestContext();
                ExtentTest testContext = (ExtentTest) iTestContext.getAttribute("testContext");
                ExtentTest test = testContext.createNode(iTestResult.getName(), iInvokedMethod.getTestMethod().getDescription());
                iTestResult.setAttribute("test", test);
            }
        }
    
        public void afterInvocation(IInvokedMethod iInvokedMethod, ITestResult iTestResult) {
            if (iInvokedMethod.isTestMethod()) {
                ExtentTest test = (ExtentTest) iTestResult.getAttribute("test");
                List<String> logs = Reporter.getOutput(iTestResult);
                for (String log : logs) {
                    test.info(log);
                }
    
                int status = iTestResult.getStatus();
                if (ITestResult.SUCCESS == status) {
                    test.pass("Passed");
                } else if (ITestResult.FAILURE == status) {
                    test.fail(iTestResult.getThrowable());
                } else {
                    test.skip("Skipped");
                }
    
                for (String group : iInvokedMethod.getTestMethod().getGroups()) {
                    test.assignCategory(group);
                }
            }
        }
    
        /**
         * Adds a screen shot image file to the report. This method should be used only in the configuration method
         * and the {@link ITestResult} is the mandatory parameter
         *
         * @param iTestResult The {@link ITestResult} object
         * @param filePath    The image file path
         * @throws IOException {@link IOException}
         */
        public void addScreenCaptureFromPath(ITestResult iTestResult, String filePath) throws IOException {
            ExtentTest test = (ExtentTest) iTestResult.getAttribute("test");
            test.addScreenCaptureFromPath(filePath);
        }
    
        /**
         * Adds a screen shot image file to the report. This method should be used only in the
         * {@link org.testng.annotations.Test} annotated method
         *
         * @param filePath The image file path
         * @throws IOException {@link IOException}
         */
        public void addScreenCaptureFromPath(String filePath) throws IOException {
            ITestResult iTestResult = Reporter.getCurrentTestResult();
            Preconditions.checkState(iTestResult != null);
            ExtentTest test = (ExtentTest) iTestResult.getAttribute("test");
            test.addScreenCaptureFromPath(filePath);
        }
    
        /**
         * Sets the test runner output
         *
         * @param message The message to be logged
         */
        public void setTestRunnerOutput(String message) {
            testRunnerOutput.add(message);
        }
    
        public void generateReport(List<XmlSuite> list, List<ISuite> list1, String s) {
            if (getSystemInfo() != null) {
                for (Map.Entry<String, String> entry : getSystemInfo().entrySet()) {
                    reporter.setSystemInfo(entry.getKey(), entry.getValue());
                }
            }
            reporter.setTestRunnerOutput(testRunnerOutput);
            reporter.flush();
        }
    
        /**
         * Adds the new node to the test. The node name should have been set already using {@link NodeName}
         */
        public void addNewNodeToTest() {
            addNewNodeToTest(NodeName.getNodeName());
        }
    
        /**
         * Adds the new node to the test with the given node name.
         *
         * @param nodeName The name of the node to be created
         */
        public void addNewNodeToTest(String nodeName) {
            addNewNode("test", nodeName);
        }
    
        /**
         * Adds a new node to the suite. The node name should have been set already using {@link NodeName}
         */
        public void addNewNodeToSuite() {
            addNewNodeToSuite(NodeName.getNodeName());
        }
    
        /**
         * Adds a new node to the suite with the given node name
         *
         * @param nodeName The name of the node to be created
         */
        public void addNewNodeToSuite(String nodeName) {
            addNewNode(SUITE_ATTR, nodeName);
        }
    
        private void addNewNode(String parent, String nodeName) {
            ITestResult result = Reporter.getCurrentTestResult();
            Preconditions.checkState(result != null);
            ExtentTest parentNode = (ExtentTest) result.getAttribute(parent);
            ExtentTest childNode = parentNode.createNode(nodeName);
            result.setAttribute(nodeName, childNode);
        }
    
        /**
         * Adds a info log message to the node. The node name should have been set already using {@link NodeName}
         *
         * @param logMessage The log message string
         */
        public void addInfoLogToNode(String logMessage) {
            addInfoLogToNode(logMessage, NodeName.getNodeName());
        }
    
        /**
         * Adds a info log message to the node
         *
         * @param logMessage The log message string
         * @param nodeName   The name of the node
         */
        public void addInfoLogToNode(String logMessage, String nodeName) {
            ITestResult result = Reporter.getCurrentTestResult();
            Preconditions.checkState(result != null);
            ExtentTest test = (ExtentTest) result.getAttribute(nodeName);
            test.info(logMessage);
        }
    
        /**
         * Marks the node as failed. The node name should have been set already using {@link NodeName}
         *
         * @param t The {@link Throwable} object
         */
        public void failTheNode(Throwable t) {
            failTheNode(NodeName.getNodeName(), t);
        }
    
        /**
         * Marks the given node as failed
         *
         * @param nodeName The name of the node
         * @param t        The {@link Throwable} object
         */
        public void failTheNode(String nodeName, Throwable t) {
            ITestResult result = Reporter.getCurrentTestResult();
            Preconditions.checkState(result != null);
            ExtentTest test = (ExtentTest) result.getAttribute(nodeName);
            test.fail(t);
        }
    
        /**
         * Marks the node as failed. The node name should have been set already using {@link NodeName}
         *
         * @param logMessage The message to be logged
         */
        public void failTheNode(String logMessage) {
            failTheNode(NodeName.getNodeName(), logMessage);
        }
    
        /**
         * Marks the given node as failed
         *
         * @param nodeName   The name of the node
         * @param logMessage The message to be logged
         */
        public void failTheNode(String nodeName, String logMessage) {
            ITestResult result = Reporter.getCurrentTestResult();
            Preconditions.checkState(result != null);
            ExtentTest test = (ExtentTest) result.getAttribute(nodeName);
            test.fail(logMessage);
        }
    }
    
    class MyReporter {
        public static ExtentTest report;
        private static String testName;
    
        public static String getTestName() {
            return testName;
        }
    
        public static void setTestName(String testName) {
            MyReporter.testName = testName;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252
    • 253
    • 254
    • 255
    • 256
    • 257
    • 258
    • 259
    • 260
    • 261
    • 262
    • 263
    • 264
    • 265
    • 266
    • 267
    • 268
    • 269
    • 270
    • 271
    • 272
    • 273
    • 274
    • 275
    • 276
    • 277
    • 278
    • 279
    • 280
    • 281
    • 282
    • 283
    • 284
    • 285
    • 286
    • 287
    • 288
    • 289
    • 290
    • 291
    • 292
    • 293
    • 294
    • 295
    • 296
    • 297
    • 298
    • 299
    • 300
    • 301
    • 302
    • 303
    • 304
    • 305
    • 306
    • 307
    • 308
    • 309
    • 310
    • 311
    • 312
    • 313
    • 314
    • 315
    • 316
    • 317
    • 318
    • 319
    • 320
    • 321
    • 322
    • 323
    • 324
    • 325
    • 326
    • 327
    • 328
    • 329
    • 330
    • 331
    • 332
    • 333
    • 334
    • 335
    • 336
    • 337
    • 338
    • 339
    • 340
    • 341
    • 342
    • 343
    • 344
    • 345
    • 346
    • 347
    • 348
    • 349
    • 350
    • 351
    • 352
    • 353
    • 354
    • 355
    • 356
    • 357
    • 358
    • 359
    • 360
    • 361
    • 362
    • 363
    • 364
    • 365
    • 366
    • 367
    • 368
    • 369
    • 370

    Step-3:配置监听

    在测试集合 testng.xml 文件中导入 Listener 监听类。

    <listeners> 
        <listener class-name="com.zuozewei.extentreportdemo.listener.MyExtentTestNgFormatter"/> 
        listeners>
    
    • 1
    • 2
    • 3

    Step-4:配置报告

    extent reporters支持报告的配置。目前支持的配置内容有title、主题等。 先在 src/resources/目录下添加 config/report/extent-config.xml

    
    <extentreports>
        <configuration>
            <timeStampFormat>yyyy-MM-dd HH:mm:sstimeStampFormat>
            
            
            <theme>darktheme>
    
            
            
            <encoding>UTF-8encoding>
    
            
            
            <protocol>httpsprotocol>
    
            
            <documentTitle>接口自动化测试报告documentTitle>
    
            
            <reportName>接口自动化测试报告reportName>
    
            
            <reportHeadline>接口自动化测试报告reportHeadline>
    
            
            
            <dateFormat>yyyy-MM-dddateFormat>
    
            
            
            <timeFormat>HH:mm:sstimeFormat>
    
            
            <scripts>
                
            scripts>
    
            
            <styles>
                
            styles>
        configuration>
    extentreports>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    Step-5:配置系统

    config下新建 MySystemInfo类继承 SystemInfo 接口

    public class MySystemInfo implements SystemInfo {
        @Override
        public Map<String, String> getSystemInfo() {
    
            Map<String, String> systemInfo = new HashMap<>();
            systemInfo.put("测试人员", "zuozewei");
    
            return systemInfo;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    可用于添加系统信息,例如:db的配置信息,人员信息,环境信息等。根据项目实际情况添加。

    至此,extentreports 美化报告完成。

    Step-6:添加测试用例

    public class TestMethodsDemo {
    
        @Test
        public void test1(){
            Assert.assertEquals(1,2);
        }
    
        @Test
        public void test2(){
            Assert.assertEquals(1,1);
        }
    
    
        @Test
        public void test3(){
            Assert.assertEquals("aaa","aaa");
        }
    
    
        @Test
        public void logDemo(){
            Reporter.log("这是故意写入的日志");
            throw new RuntimeException("故意运行时异常");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    Step-7:测试用例suite

    DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >
    
    <suite name="测试demo" verbose="1" preserve-order="true">
        <parameter name="report.config" value="src/main/resources/report/extent-config.xml"/>
        <parameter name="system.info" value="com.zuozewei.extentreportdemo.config.MySystemInfo"/>
    
        <test name="测试demo" preserve-order="true">
            <classes>
                <class name="com.zuozewei.extentreportdemo.testCase.TestMethodsDemo"/>
            classes>
        test>
    
        <listeners>
            <listener class-name="com.zuozewei.extentreportdemo.listener.MyExtentTestNgFormatter"/>
        listeners>
    suite>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    测试报告

    HTML Resport 示例

    Email Report 示例

    工程目录

    本文源码:

    https://github.com/7DGroup/Java-API-Test-Examples

  • 相关阅读:
    无状态java服务在k8s下流量无缝切换
    【Spring】Bean的装配与管理
    C++练习:类和对象
    Ascend910训练CSWin-Transformer失败
    Django框架FAQ
    五三想休息,今天还学习,图解二叉树的层序遍历BFS(广度优先)模板,附面试题题解
    CSS清除浮动解决方案
    【可变形注意力(1)】Multi-scale Deformable Attention Transformers 多尺度变形注意力
    [C++]类和对象(重中之重)
    python基于PHP的个人信息管理系统
  • 原文地址:https://blog.csdn.net/qwerasdctds/article/details/136207577