自动化工具 ExtentReports 结合 TestNg 生成自动化 html 报告 (支持多 suite)

sen · 2017年03月28日 · 最后由 Dreamslians-github 回复于 2019年07月08日 · 5093 次阅读

重要说明:报告监听器源码修复一些 bug,不再此处更新代码,最新代码可以到 github 查看最新报告监听器源码

前几天分享了http (s) 接口自动化测试框架 (基于 java),用的是 ReportNg 来生成报告,@532589730 同学推荐了下 extentreport,这几天看了下了解下,做个分享,准备引入到框架中,废话不多说,开始。

说明

官网地址

  1. 使用 TestNg 的 Report 监听器,不嵌入具体执行代码,仅需在配置文件中新增监听器即可。
  2. 报告文件生成路径为 test-output/index.html。(可在代码中修改)
  3. 一个 suite 且一个 test 配置的情况下,会将执行的用例 (method) 作为一级节点生成报告。
  4. 一个 suite 且多个 test 配置的情况下,会将每个 test 配置作为一级节点,执行用例 (method) 为对应的子节点
  5. 多个 suite 的情况下,将 suite 作为一级节点,test 配置为二级节点,执行用例 (method) 为对应的三级节点。(如果 suite 下只有一个 test 配置,则不会生成二级节点,直接把执行的用例 (method) 生成在第二节点中)
  6. 代码中使用 Report.log("xxx") 会将 log 展示在报告中对应的执行用例 (method) 中。
  7. 自动将 suite 以及 test 配置的名字作为执行用例 (method) 的标签。
  8. 如果用例 (method) 有参数,则会将调用参数的 toString() 方法作为用例 (method) 的名字在报告中显示。
  9. 已经对执行用例进行按时间排序。(但是多个 suite 按时间的排序不知道咋处理,求指教。)

代码

pom 引用:

<dependency>
   <groupId>com.aventstack</groupId>
  <artifactId>extentreports</artifactId>
  <version>3.0.3</version>
</dependency>

创建 TestNg 的 Report 监听器:

import com.aventstack.extentreports.ExtentReports;
import com.aventstack.extentreports.ExtentTest;
import com.aventstack.extentreports.Status;
import com.aventstack.extentreports.model.TestAttribute;
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.*;

/**
 * Created by chenwx on 17/3/24.
 */
public class ExtentTestNGIReporterListener implements IReporter {
    //生成的路径以及文件名
    private static final String OUTPUT_FOLDER = "test-output/";
    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.mkdir();
        }
        ExtentHtmlReporter htmlReporter = new ExtentHtmlReporter(OUTPUT_FOLDER + FILE_NAME);
        htmlReporter.config().setDocumentTitle("api自动化测试报告");
        htmlReporter.config().setReportName("api自动化测试报告");
        htmlReporter.config().setChartVisibilityOnOpen(true);
        htmlReporter.config().setTestViewChartLocation(ChartLocation.TOP);
        // htmlReporter.config().setTheme(Theme.STANDARD);
        htmlReporter.config().setResourceCDN(ResourceCDN.EXTENTREPORTS);
        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){
                    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();
    }
}

testng 配置文件新增监听器:

<listeners>
  <!-- class-name的值填写为时间创建的监听器的路径 -->
  <listener class-name="test.sen.example.ExtentTestNGIReporterListener"></listener>
</listeners>

例子:

测试代码:

package test.sen.example;

import org.testng.Assert;
import org.testng.Reporter;
import org.testng.annotations.BeforeSuite;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;


/**
 * Created by chenwx on 17/3/22.
 */
public class ReportTest {

    @DataProvider(name = "createData")
    public Iterator<Object[]> createData(){
        List<Object[]> dataProvider = new ArrayList<Object[]>();
        for (int i=0;i<2;i++){
            String[] s = {String.format("我是第(%s)个参数",i)};
            dataProvider.add(s);
        }
        return  dataProvider.iterator();
    }

    @Test(dataProvider = "createData")
    public void dataProviderTest(String s){
        //输出log会在报告中提现
        Reporter.log("获取到参数:"+s,true);
        Assert.assertTrue(s.length()>2," 成功?失败?");
    }

    @Test
    public void testTrue() {
        Assert.assertTrue(true,"成功咯!");
    }

    @Test
    public void testFail() {
        Assert.fail("失败咯!");
    }
}

testng-suite1.xml:单 suite 多 test

<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >
<suite name="suite1下有多test" verbose="1" preserve-order="true" parallel="false">
    <test name="有参数的用例的test">
        <classes>
            <class name="test.sen.example.ReportTest">
                <methods>
                    <include name="dataProviderTest"></include>
                </methods>
            </class>
        </classes>
    </test>
    <test name="成功的test">
        <classes>
            <class name="test.sen.example.ReportTest">
                <methods>
                    <include name="testTrue"></include>
                </methods>
            </class>
        </classes>
    </test>
    <listeners>
        <listener class-name="test.sen.example.ExtentTestNGIReporterListener"></listener>
        <!--<listener class-name="test.sen.example.ExtentTestNGITestListener"></listener>-->
    </listeners>
</suite>

执行结果:
单suite多test

testng-suite2.xml:单 suite 单 test

<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >
<suite name="suite2只有一个test" verbose="1" preserve-order="true" parallel="false">
    <test name="失败用例">
        <classes>
            <class name="test.sen.example.ReportTest">
                <methods>
                    <include name="testFail"></include>
                </methods>
            </class>
        </classes>
    </test>
    <listeners>
        <listener class-name="test.sen.example.ExtentTestNGIReporterListener"></listener>
        <!--<listener class-name="test.sen.example.ExtentTestNGITestListener"></listener>-->
    </listeners>
</suite>

执行结果:单suite单test

testng.xml:多 suite 多 test

<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >
<suite name="多个suite测试" verbose="1" preserve-order="true" parallel="false">
    <suite-files>
        <suite-file path="testng-suite1.xml"></suite-file>
        <suite-file path="testng-suite2.xml"></suite-file>
    </suite-files>
    <listeners>
        <listener class-name="test.sen.example.ExtentTestNGIReporterListener"></listener>
        <!--<listener class-name="test.sen.example.ExtentTestNGITestListener"></listener>-->
    </listeners>
</suite>

执行结果:
多suite多test

最后,推下我的公众号

关注公众号

共收到 69 条回复 时间 点赞
Unmurphy ExtentReports 测试报告框架简单总结介绍 中提及了此贴 04月11日 14:53

运行测试代码,就会生成 html 报告吗?按照你的方法,我跑完 testcase 后,生成还是之前的 testng report

sen #3 · 2017年04月13日 Author
jiap 回复

如果你是 IDE 工具直接执行 test 方法,需要修改 IDE 的 TestNg 监听器配置。
如果是执行 testng.xml 配置文件,则需要在配置文件增加配置

楼主,你的我这样配置后:

<test name="登录">
    <classes>
        <class name="test.LoginPageTest"/>
    </classes>
</test>
<test name="个人页面">
    <classes>
        <class name="test.MyinfoPageTest"/>
    </classes>
</test>

生成测试报告,分类那里出现了重复:

我的生成不了 index.html 文件

按您上面的方式创建了监听器,并在 testng.xml 中增加了监听配置, 执行完成后, new ExtentHtmlReporter 报错..是为什么呀
java.lang.NoClassDefFoundError: freemarker/template/TemplateModelException
ExtentTestNGIReporterListener.init(ExtentTestNGIReporterListener.java:49)

问题已解决,缺 freemarker 包,补上可以生成

sen #7 · 2017年06月12日 Author

TesterHome 属于一级标签,登录以及个人页面属于二级标签。所以 TesterHome 上面显示的是总的。

sen #8 · 2017年06月12日 Author
waangyu 回复

具体什么情况呢?

// adding screenshots to log
test.fail("details", MediaEntityBuilder.createScreenCaptureFromPath("screenshot.png").build());

// or:
MediaModelProvider mediaModel = MediaEntityBuilder.createScreenCaptureFromPath("screenshot.png").build();
test.fail("details", mediaModel);

// adding screenshots to test
test.fail("details").addScreenCaptureFromPath("screenshot.png");

楼主,截图在报告中显示加了吗?

sen #11 · 2017年07月05日 Author

没处理截图,如果需要可以在我的代码基础上修改。

gdx 回复

已解决😁

sen #14 · 2017年07月17日 Author
gdx 回复

恩恩,git 上最新的代码也已经解决此问题。

gdx 回复

请问下,css 和 js 怎么配置的

sen #16 · 2017年08月08日 Author
lozz 回复

这个是用的是 ExentReport 官方线上的报告。
可以参考我实际应用的地方:https://github.com/ChenSen5/api_autotest/blob/master/src/main/java/com/sen/api/listeners/ExtentTestNGIReporterListener.java

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

jiap 回复

请问您解决,extentreport 的样式问题?

LZ,请问为何我使用你的代码,同样的代码在 eclipse 和 IDEA 中生成的 index.html 是不一样的。

sen #19 · 2017年08月24日 Author
cooling 回复

样式我最新 github 上得代码没问题哦!

sen #20 · 2017年08月24日 Author
cooling 回复

如果你是通过执行 testng.xml 配置文件的画,请检查下配置是否添加监听器。
如果是直接执行测试方法的话,可以在你的测试类加上@Listener监听器,或者在 eclipse 上的 testng 配置上添加监听器路径。

sen 回复

我在项目的 testng 中指定了 listener,如下图,就实现了在 eclipse 中能完全显示你的实例代码。


----LZ,你看看我用的这样的方法可行?

sen #22 · 2017年08月24日 Author
cooling 回复

需要把包得路径也补充上。ex1.xxxListener

sen 回复

OK ,3Q

sen 回复

大神,testng 乱码怎么破,我百度过,设置还是不得行。

sen #25 · 2017年08月28日 Author
cooling 回复

我猜是你的文件本身编码有问题?另存为 utf-8 试试?

sen 回复

我试了,还是不行

是不是最新版本不支持离线报告?

sen #28 · 2017年09月08日 Author
infy001 回复

三开始免费版不支持离线

楼主,我生成的报告直接浏览器打开的话,中文是乱码,记事本另存为修改下编码 utf-8 后才能显示正常,请问怎么解决啊?

sen #30 · 2017年09月12日 Author
大毛 回复

默认就是使用的 utf-8 编码的哦,我没有遇到这类情况,或许你可以用最新的版本试试。

<dependency>
    <groupId>com.aventstack</groupId>
    <artifactId>extentreports</artifactId>
    <version>3.0.7</version>
</dependency>
sen 回复

多谢啦。

如果 TestNG 加入了失败重跑呢? 第二次再次失败才要记录,第一次失败是不记录。

sen #33 · 2017年10月26日 Author

重跑是会被 testng 记录的,所以你如果想第二次失败才记录那你要实现下 TestListenerAdapter,在 onFinish 方法下实现你的需求,移除掉第一个记录。

sen 回复

我 Testng 的 report 我移除了重复的 case,但是 ExtentReports 依旧两个。

@chen_sen 楼主,ExtentReport 3.0+ 版本是否还需要加载官方提供的 Klov Report,或者设置本地化报告?
我用 3+,按照你的代码敲下来,报告排版会比较乱(未加载完全的样子)。
使用 2+ 的就会正常显示,求解 3 如何处理?

sen #36 · 2017年11月22日 Author
碎冰之殇 回复

最近有点忙,很久没有上 TesterHome。回复慢了,不好意思哈,可以叫我微信:chen5xian。
我用的也是 3.0,如果为加载完毕,看看是否 JS 以及 CSS 没有正常加载。

sen #37 · 2017年11月22日 Author
米阳MeYoung 回复

要看你时怎么移除的,方便贴实现代码?

sen 回复

谢谢,我后来自己阅读了下代码,发现只要在你的代码里面 buildTestNodes() 里面加个逻辑,就可以把重复的 case 移除了。

sen #39 · 2017年11月22日 Author
米阳MeYoung 回复

👍

我这边生成的报告,初始页那两个饼图testssteps数据是一样的 为什么呢?testssteps是怎么跟testng.xml对应的呢?求大神指导

sen #41 · 2017年12月20日 Author
huangwenbin 回复

框架会把 test 的上层(suite)也会当成一个 step

@chen_sen 楼主你好,我是按照你的代码执行的



但是最后报告统计的时候,会吧 test 都统计了,有些重复,怎么可以不统计 test 呢

sen #43 · 2018年01月01日 Author
wangmcn 回复

确实会被统计进去,目前还想到怎么去除。


楼主,这个截图获取的路径我应该怎么修改下。

sen #45 · 2018年02月23日 Author
andrew.gao 回复

src 使用相对路径。
如把图片放在 html 文件同级目录,然后 src 直接是文件名.png 就可以了。

Good,感谢分享!

请问下楼主,那个会多余统计 test 的问题解决了吗?

sen #48 · 2018年03月06日 Author
耒蒙 回复

还没呢,有兴趣的同学可以研究下怎么解决好。😂

sen 回复

再请教您一个问题,测试报告,我自己这边样式加载完好,发给同事,他们都无法正常加载样式,请问你有遇到这个问题吗?

sen #50 · 2018年03月07日 Author
耒蒙 回复

1、打开控制台看看报什么错。有可能是 JS 加载不出来。

sen 回复

是的了,这两个文件加载不了,我自己清理了浏览器缓存后,我也加载不了了,尴尬....

sen #52 · 2018年03月08日 Author
耒蒙 回复

到 gitbub 上看我最新的代码吧,

主要改动点是:
htmlReporter.config().setTheme(Theme.STANDARD);
更改为:
htmlReporter.config().setResourceCDN(ResourceCDN.EXTENTREPORTS);

此行为修改 JS/CSS 的源路径。

大毛 回复

我用 maven 打包然后跑的用例也是中文乱码,得把测试报告网页重新 UTF-8 with BOM 保存就好了

sen #54 · 2018年03月11日 Author
耒蒙 回复

正常应该不会才对,我没有遇见过乱码的情况,不知道是不是 exent 的版本不对。

<dependency>
    <groupId>com.aventstack</groupId>
    <artifactId>extentreports</artifactId>
    <version>3.0.6</version>
</dependency>
仅楼主可见
sen #56 · 2018年04月04日 Author

可以到 gitbub 上看我最新的代码吧,

主要改动点是:

htmlReporter.config().setTheme(Theme.STANDARD);

更改为:

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

此行为修改 JS/CSS 的源路径。

htmlReporter.config().setResourceCDN(ResourceCDN.EXTENTREPORTS);
没有 setResourceCDN 这个方法怎么办,在线等

sen #58 · 2018年04月10日 Author
TesterRoad 回复

拿我上面贴的报告监听器源代码不行吗?

sen 回复

不行的,直接报错,而且我看了没 setResourceCDN 这个方法,楼主可否加个联系方式呢

sen #60 · 2018年04月11日 Author
TesterRoad 回复

微信:chen5xian

楼主,gitub 上拉了最新代码,在 testng 的 suite 上也加上了 ExtentTestNGIReporterListener,但是没有生成 index.html

没有弄明白这个具体是要怎么用啊?下载了源码,将 com 整个包放在了 test 文件夹里面。也添加了 dependencies,但是 testng.xml 中总是显示这个 listner 是红色的。。。

cooling 回复

修改开发工具全局变量的编码格式 不能单单修改项目工程的。

修改开发工具全局的编码格式 不能单单修改项目工程的。

65楼 已删除

3.x 支持 java8.

ExtentReports 报告乱码的问题:

  • 如果你的工程编码是:UTF-8,则 ExtentReports 需要配置为:GBK

为啥我这边加了 provider 后就不能生成呢

@chen_sen 我这边加了 provider 后就没法生成 ExtentReports 报告,不加可以,怎么回事,完全 copy 你的代码

sen 回复

楼主,这个多算一个 case 的问题想问问解决了么?

wangmcn 回复

hello,你的环境怎么显示出来的?

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册