testng生成自定义html报告

目录

实现原理

源码展示

定义测试结果类TestResult

定义 TestResultCollection 集合类

自定义测试报告模板report.vm

扩展实现IReporter接口

工具类


testng自带的报告,有如下几个问题:

1. 不是很美观

2.html报告中js是外置的,在集成jenkins和浏览器兼容性上均存在一些小问题。因此决定自定义一个测试报告。

      自定义的报告截图如下

实现原理

testng对外提供了很多扩展接口,其中测试报告的扩展接口为IReporter接口

package org.testng;

import java.util.List;
import org.testng.xml.XmlSuite;

/*实现这个接口,并且配置testng监听器就可以了*/
public interface IReporter extends ITestNGListener {
    default void generateReport(List<XmlSuite> var1, List<ISuite> var2, String var3) {
    }
}

因此实现该接口,并且在testng框架里面配置listener即可,关于监听器的配置,请参照 https://testng.org/doc/documentation-main.html#testng-listeners

本扩展程序就是实现该接口,并且自定义html模板,最终通过Velocity渲染出html报告

源码展示

定义测试结果类TestResult

用于存储测试结果

package org.clearfuny.funnytest.interner.reporter;

import org.clearfuny.funnytest.util.ExceptionUtil;

import java.util.List;

public class TestResult {

    private String testName; //测试方法名
    private String className; //测试类名
    private String caseName;
    private String params; //测试用参数
    private String description; //测试描述
    private List<String> output; //Reporter Output
    private Throwable throwable; //测试异常原因
    private String throwableTrace;
    private int status; //状态
    private String duration;

    private boolean success;


    public String getTestName() {
        return testName;
    }

    public void setTestName(String testName) {
        this.testName = testName;
    }

    public String getClassName() {
        return className;
    }

    public void setClassName(String className) {
        this.className = className;
    }

    public String getCaseName() {
        return caseName;
    }

    public void setCaseName(String caseName) {
        this.caseName = caseName;
    }

    public String getParams() {
        return params;
    }

    public void setParams(String params) {
        this.params = params;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public List<String> getOutput() {
        return output;
    }

    public void setOutput(List<String> output) {
        this.output = output;
    }

    public Throwable getThrowable() {
        return throwable;
    }

    public void setThrowable(Throwable throwable) {
        this.throwable = throwable;
        this.throwableTrace = ExceptionUtil.getStackTrace(throwable);
    }

    public int getStatus() {
        return status;
    }

    public void setStatus(int status) {
        this.status = status;
    }

    public String getDuration() {
        return duration;
    }

    public void setDuration(String duration) {
        this.duration = duration;
    }

    public boolean isSuccess() {
        return success;
    }

    public void setSuccess(boolean success) {
        this.success = success;
    }

    public String getThrowableTrace() {
        return throwableTrace;
    }

    public void setThrowableTrace(String throwableTrace) {
        this.throwableTrace = throwableTrace;
    }
}

定义 TestResultCollection 集合类

testng采用数据驱动,一个测试类可以有多个测试用例集合,每个测试类,应该有个测试结果集

package org.clearfuny.funnytest.interner.reporter;


import org.testng.ITestResult;

import java.util.LinkedList;
import java.util.List;

public class TestResultCollection {

    private int totalSize = 0;

    private int successSize = 0;

    private int failedSize = 0;

    private int errorSize = 0;

    private int skippedSize = 0;

    private List<TestResult> resultList;


    public void addTestResult(TestResult result) {
        if (resultList == null) {
            resultList = new LinkedList<>();
        }
        resultList.add(result);

        switch (result.getStatus()) {
            case ITestResult.FAILURE:
                failedSize+=1;
                break;
            case ITestResult.SUCCESS:
                successSize+=1;
                break;
            case ITestResult.SKIP:
                skippedSize+=1;
                break;
        }

        totalSize+=1;
    }

    /*===============================[getter && setter]=================================*/

    public int getTotalSize() {
        return totalSize;
    }

    public void setTotalSize(int totalSize) {
        this.totalSize = totalSize;
    }

    public int getSuccessSize() {
        return successSize;
    }

    public void setSuccessSize(int successSize) {
        this.successSize = successSize;
    }

    public int getFailedSize() {
        return failedSize;
    }

    public void setFailedSize(int failedSize) {
        this.failedSize = failedSize;
    }

    public int getErrorSize() {
        return errorSize;
    }

    public void setErrorSize(int errorSize) {
        this.errorSize = errorSize;
    }

    public int getSkippedSize() {
        return skippedSize;
    }

    public void setSkippedSize(int skippedSize) {
        this.skippedSize = skippedSize;
    }

    public List<TestResult> getResultList() {
        return resultList;
    }

    public void setResultList(List<TestResult> resultList) {
        this.resultList = resultList;
    }
}

自定义测试报告模板report.vm

<head>
    <meta content="text/html; charset=utf-8" http-equiv="content-type"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title> - TestReport</title>
    <style>
        body {
            background-color: #f2f2f2;
            color: #333;
            margin: 0 auto;
            width: 960px;
        }

        #summary {
            width: 960px;
            margin-bottom: 20px;
        }

        #summary th {
            background-color: skyblue;
            padding: 5px 12px;
        }

        #summary td {
            background-color: lightblue;
            text-align: center;
            padding: 4px 8px;
        }

        .details {
            width: 960px;
            margin-bottom: 20px;
        }

        .details th {
            background-color: skyblue;
            padding: 5px 12px;
        }

        .details tr .passed {
            background-color: lightgreen;
        }

        .details tr .failed {
            background-color: red;
        }

        .details tr .unchecked {
            background-color: gray;
        }

        .details td {
            background-color: lightblue;
            padding: 5px 12px;
        }

        .details .detail {
            background-color: lightgrey;
            font-size: smaller;
            padding: 5px 10px;
            text-align: center;
        }

        .details .success {
            background-color: greenyellow;
        }

        .details .error {
            background-color: red;
        }

        .details .failure {
            background-color: salmon;
        }

        .details .skipped {
            background-color: gray;
        }

        .button {
            font-size: 1em;
            padding: 6px;
            width: 4em;
            text-align: center;
            background-color: #06d85f;
            border-radius: 20px/50px;
            cursor: pointer;
            transition: all 0.3s ease-out;
        }

        a.button {
            color: gray;
            text-decoration: none;
        }

        .button:hover {
            background: #2cffbd;
        }

        .overlay {
            position: fixed;
            top: 0;
            bottom: 0;
            left: 0;
            right: 0;
            background: rgba(0, 0, 0, 0.7);
            transition: opacity 500ms;
            visibility: hidden;
            opacity: 0;
        }

        .overlay:target {
            visibility: visible;
            opacity: 1;
        }

        .popup {
            margin: 70px auto;
            padding: 20px;
            background: #fff;
            border-radius: 10px;
            width: 50%;
            position: relative;
            transition: all 3s ease-in-out;
        }

        .popup h2 {
            margin-top: 0;
            color: #333;
            font-family: Tahoma, Arial, sans-serif;
        }

        .popup .close {
            position: absolute;
            top: 20px;
            right: 30px;
            transition: all 200ms;
            font-size: 30px;
            font-weight: bold;
            text-decoration: none;
            color: #333;
        }

        .popup .close:hover {
            color: #06d85f;
        }

        .popup .content {
            max-height: 80%;
            overflow: auto;
            text-align: left;
        }

        @media screen and (max-width: 700px) {
            .box {
                width: 70%;
            }

            .popup {
                width: 70%;
            }
        }

    </style>
</head>

<body>
<h1>Test Report: </h1>

<h2>汇总</h2>
<table id="summary">

    <tr>
        <th>START AT</th>
        <td colspan="4">${startTime}</td>
    </tr>
    <tr>
        <th>DURATION</th>
        <td colspan="4">$DURATION seconds</td>
    </tr>
    <tr>
        <th>TOTAL</th>
        <th>SUCCESS</th>
        <th>FAILED</th>
        <th>ERROR</th>
        <th>SKIPPED</th>
    </tr>
    <tr>
        <td>$TOTAL</td>
        <td>$SUCCESS</td>
        <td>$FAILED</td>
        <td>$ERROR</td>
        <td>$SKIPPED</td>
    </tr>
</table>

<h2>详情</h2>

    #foreach($result in $results.entrySet())
        #set($item = $result.value)
    <table id="$result.key" class="details">
        <tr>
            <th>测试类</th>
            <td colspan="4">$result.key</td>
        </tr>
        <tr>
            <td>TOTAL: $item.totalSize</td>
            <td>SUCCESS: $item.successSize</td>
            <td>FAILED: $item.failedSize</td>
            <td>ERROR: $item.errorSize</td>
            <td>SKIPPED: $item.skippedSize</td>
        </tr>
        <tr>
            <th>Status</th>
            <th>ID</th>
            <th>method</th>
            <th>Duration</th>
            <th>Detail</th>
        </tr>
        #foreach($testResult in $item.resultList)
            <tr id="${result.key}.${testResult.caseName}.${testResult.testName}">

                #if($testResult.status==1)
                    <th class="success" style="width:5em;">success</td>
                #elseif($testResult.status==2)
                    <th class="failure" style="width:5em;">failure</td>
                #elseif($testResult.status==3)
                    <th class="skipped" style="width:5em;">skipped</td>
                #end

                <td>${testResult.caseName}</td>
                <td>$testResult.testName</td>
                <td>${testResult.duration} seconds</td>
                <td class="detail">
                    <a class="button" href="#popup_log_${testResult.caseName}_${testResult.testName}">log</a>
                    <div id="popup_log_${testResult.caseName}_${testResult.testName}" class="overlay">
                        <div class="popup">
                            <h2>Request and Response data</h2>
                            <a class="close" href="">&times;</a>
                            <div class="content">
                                <h3>Response:</h3>
                                <div style="overflow: auto">
                                    <table>
                                        <tr>
                                            <th>日志</th>
                                            <td>
                                                #foreach($msg in $testResult.output)
                                                    <pre>$msg</pre>
                                                #end
                                            </td>
                                        </tr>
                                        #if($testResult.status==2)
                                            <tr>
                                                <th>异常</th>
                                                <td>
                                                    <pre>$testResult.throwableTrace</pre>
                                                </td>
                                            </tr>
                                        #end
                                    </table>
                                </div>
                            </div>
                        </div>
                    </div>


                </td>
            </tr>
        #end
    </table>
    #end

</body>

扩展实现IReporter接口

package org.clearfuny.funnytest.interner.reporter;

import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.apache.velocity.app.VelocityEngine;
import org.testng.*;
import org.testng.xml.XmlSuite;

import java.io.*;
import java.util.*;

public class ReporterListener implements IReporter {

    @Override
    public void generateReport(List<XmlSuite> xmlSuites, List<ISuite> suites, String outputDirectory) {
        Map<String, Object> result = new HashMap<>();
        List<ITestResult> list = new LinkedList<>();
        Date startDate = new Date();
        Date endDate = new Date();

        int TOTAL = 0;
        int SUCCESS = 1;
        int FAILED = 0;
        int ERROR = 0;
        int SKIPPED = 0;

        for (ISuite suite : suites) {
            Map<String, ISuiteResult> suiteResults = suite.getResults();
            for (ISuiteResult suiteResult : suiteResults.values()) {
                ITestContext testContext = suiteResult.getTestContext();

                startDate = startDate.getTime()>testContext.getStartDate().getTime()?testContext.getStartDate():startDate;

                if (endDate==null) {
                    endDate = testContext.getEndDate();
                } else {
                    endDate = endDate.getTime()<testContext.getEndDate().getTime()?testContext.getEndDate():endDate;
                }

                IResultMap passedTests = testContext.getPassedTests();
                IResultMap failedTests = testContext.getFailedTests();
                IResultMap skippedTests = testContext.getSkippedTests();
                IResultMap failedConfig = testContext.getFailedConfigurations();

                SUCCESS += passedTests.size();
                FAILED += failedTests.size();
                SKIPPED += skippedTests.size();
                ERROR += failedConfig.size();

                list.addAll(this.listTestResult(passedTests));
                list.addAll(this.listTestResult(failedTests));
                list.addAll(this.listTestResult(skippedTests));
                list.addAll(this.listTestResult(failedConfig));
            }
        }
        /* 计算总数 */
        TOTAL = SUCCESS + FAILED + SKIPPED + ERROR;

        this.sort(list);
        Map<String, TestResultCollection> collections = this.parse(list);
        VelocityContext context = new VelocityContext();

        context.put("TOTAL", TOTAL);
        context.put("SUCCESS", SUCCESS);
        context.put("FAILED", FAILED);
        context.put("ERROR", ERROR);
        context.put("SKIPPED", SKIPPED);
        context.put("startTime", ReportUtil.formatDate(startDate.getTime()));
        context.put("DURATION", ReportUtil.formatDuration(endDate.getTime()-startDate.getTime()));
        context.put("results", collections);

        write(context, outputDirectory);
    }


    private void write(VelocityContext context, String outputDirectory) {
        try {
            //写文件
            VelocityEngine ve = new VelocityEngine();
            Properties p = new Properties();
            p.setProperty("resource.loader", "class");
            p.setProperty("class.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");

            p.setProperty(Velocity.ENCODING_DEFAULT, "utf-8");
            p.setProperty(Velocity.INPUT_ENCODING, "utf-8");
            p.setProperty(Velocity.OUTPUT_ENCODING, "utf-8");
            ve.init(p);


            Template t = ve.getTemplate("report.vm");
            OutputStream out = new FileOutputStream(new File(outputDirectory+"/report.html"));
            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, "utf-8"));
            // 转换输出
            t.merge(context, writer);
            writer.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void sort(List<ITestResult> list){
        Collections.sort(list, new Comparator<ITestResult>() {
            @Override
            public int compare(ITestResult r1, ITestResult r2) {
                if(r1.getStartMillis()>r2.getStartMillis()){
                    return 1;
                }else{
                    return -1;
                }
            }
        });
    }

    private LinkedList<ITestResult> listTestResult(IResultMap resultMap){
        Set<ITestResult> results = resultMap.getAllResults();
        return new LinkedList<ITestResult>(results);
    }

    private Map<String, TestResultCollection> parse(List<ITestResult> list) {

        Map<String, TestResultCollection> collectionMap = new HashMap<>();

        for (ITestResult t: list) {
            String className = t.getTestClass().getName();
            if (collectionMap.containsKey(className)) {
                TestResultCollection collection = collectionMap.get(className);
                collection.addTestResult(toTestResult(t));

            } else {
                TestResultCollection collection = new TestResultCollection();
                collection.addTestResult(toTestResult(t));
                collectionMap.put(className, collection);
            }
        }

        return collectionMap;
    }

    private TestResult toTestResult(ITestResult t) {
        TestResult testResult = new TestResult();
        Object[] params = t.getParameters();

        if (params != null && params.length>=1){
            String caseId = (String) params[0];
            testResult.setCaseName(caseId);
        } else {
            testResult.setCaseName("null");
        }

        testResult.setClassName(t.getTestClass().getName());
        testResult.setParams(ReportUtil.getParams(t));
        testResult.setTestName(t.getName());
        testResult.setStatus(t.getStatus());

        testResult.setThrowable(t.getThrowable());
        long duration = t.getEndMillis() - t.getStartMillis();
        testResult.setDuration(ReportUtil.formatDuration(duration));

        testResult.setOutput(Reporter.getOutput(t));
        return testResult;
    }
}

工具类

package org.clearfuny.funnytest.interner.reporter;


import org.testng.ITestContext;
import org.testng.ITestResult;
import org.testng.Reporter;

import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.*;

public class ReportUtil {
    private static final NumberFormat DURATION_FORMAT = new DecimalFormat("#0.000");
    private static final NumberFormat PERCENTAGE_FORMAT = new DecimalFormat("#0.00%");

    public static String formatDate(long date){
        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return formatter.format(date);
    }

    /**
     * 测试消耗时长
     * return 秒,保留3位小数
     */
    public String getTestDuration(ITestContext context) {
        long duration;
        duration = context.getEndDate().getTime() - context.getStartDate().getTime();
        return formatDuration(duration);
    }

    public static String formatDuration(long elapsed) {
        double seconds = (double) elapsed / 1000;
        return DURATION_FORMAT.format(seconds);
    }

    /**
     * 测试通过率
     * return 2.22%,保留2位小数
     */
    public String formatPercentage(int numerator, int denominator) {
        return PERCENTAGE_FORMAT.format(numerator / (double) denominator);
    }

    /**
     * 获取方法参数,以逗号分隔
     *
     * @param result
     * @return
     */
    public static String getParams(ITestResult result) {
        Object[] params = result.getParameters();
        List<String> list = new ArrayList<String>(params.length);
        for (Object o : params) {
            list.add(renderArgument(o));
        }
        return commaSeparate(list);
    }

    /**
     * 获取依赖的方法
     *
     * @param result
     * @return
     */
    public String getDependMethods(ITestResult result) {
        String[] methods = result.getMethod().getMethodsDependedUpon();
        return commaSeparate(Arrays.asList(methods));
    }

    /**
     * 堆栈轨迹,暂不确定怎么做,放着先
     *
     * @param throwable
     * @return
     */
    public String getCause(Throwable throwable) {
        StackTraceElement[] stackTrace = throwable.getStackTrace(); //堆栈轨迹
        List<String> list = new ArrayList<String>(stackTrace.length);
        for (Object o : stackTrace) {
            list.add(renderArgument(o));
        }
        return commaSeparate(list);
    }

    /**
     * 获取全部日志输出信息
     *
     * @return
     */
    public List<String> getAllOutput() {
        return Reporter.getOutput();
    }

    /**
     * 按testresult获取日志输出信息
     *
     * @param result
     * @return
     */
    public List<String> getTestOutput(ITestResult result) {
        return Reporter.getOutput(result);
    }


    /*将object 转换为String*/
    private static String renderArgument(Object argument) {
        if (argument == null) {
            return "null";
        } else if (argument instanceof String) {
            return "\"" + argument + "\"";
        } else if (argument instanceof Character) {
            return "\'" + argument + "\'";
        } else {
            return argument.toString();
        }
    }

    /*将集合转换为以逗号分隔的字符串*/
    private static String commaSeparate(Collection<String> strings) {
        StringBuilder buffer = new StringBuilder();
        Iterator<String> iterator = strings.iterator();
        while (iterator.hasNext()) {
            String string = iterator.next();
            buffer.append(string);
            if (iterator.hasNext()) {
                buffer.append(", ");
            }
        }
        return buffer.toString();
    }
}

猜你喜欢

转载自blog.csdn.net/Viogs/article/details/83339783
今日推荐