生成TestNG可邮件发送的html测试报告

使用EmailableReporter2生成TestNG可邮件发送的html测试报告

本次使用TestNG为当前最新版本7.3.0,XML中的DTD文件需要使用https协议

针对源码中的EmailableReporter2进行优化,生成自定义的可邮件发送的html测试报告

生成报告样式举例:

 支持多suite,用例集为suite标题,测试用例为test标题。 

 

依赖包:

        <dependency>
            <groupId>org.testng</groupId>
            <artifactId>testng</artifactId>
            <version>7.3.0</version>
        </dependency>

 

源码:

package com.shein.dms.config;

import org.testng.*;
import org.testng.collections.Lists;
import org.testng.internal.Utils;
import org.testng.log4testng.Logger;
import org.testng.reporters.RuntimeBehavior;
import org.testng.xml.XmlSuite;
import org.testng.xml.XmlSuite.ParallelMode;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.NumberFormat;
import java.util.*;
import java.util.stream.Collectors;

import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.Files.newBufferedWriter;

/**
 * Reporter that generates a single-page HTML report of the test results.
 * 根据TestNG源码修改(org.testng.reporters.EmailableReporter2)
 */
public class EmailableReporter2 implements IReporter {
    private static final Logger LOG = Logger.getLogger(EmailableReporter2.class);

    protected PrintWriter writer;

    protected final List<SuiteResult> suiteResults = Lists.newArrayList();

    // Reusable buffer
    private final StringBuilder buffer = new StringBuilder();

    private static final String OUTPUT_FOLDER = "test-output/";

    private String fileName = "emailable-report.html";

    private String pageTitle = "测试报告";

    @Override
    public void generateReport(
            List<XmlSuite> xmlSuites, List<ISuite> suites, String outputDirectory) {
        try {
            writer = createWriter(OUTPUT_FOLDER);
        } catch (IOException e) {
            LOG.error("Unable to create output file", e);
            return;
        }
//        for (ISuite suite : suites) {
        for (int i = suites.size(); i > 0; i--) {
            ISuite suite = suites.get(i - 1);
            suiteResults.add(new SuiteResult(suite));
        }

        writeDocumentStart();
        writeHead();
        writeBody();
        writeDocumentEnd();

        writer.close();
    }

    protected PrintWriter createWriter(String outdir) throws IOException {
        new File(outdir).mkdirs();
        String jvmArg = RuntimeBehavior.getDefaultEmailableReport2Name();
        if (jvmArg != null && !jvmArg.trim().isEmpty()) {
            fileName = jvmArg;
        }
        return new PrintWriter(newBufferedWriter(new File(outdir, fileName).toPath(), UTF_8));
    }

    protected void writeDocumentStart() {
        writer.println(
                "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" \"https://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">");
        writer.println("<html xmlns=\"https://www.w3.org/1999/xhtml\">");
    }

    protected void writeHead() {
        writer.println("<head>");
        writer.println("<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\"/>");
//    writer.println("<title>TestNG Report</title>");
        writer.println("<title>" + pageTitle + "</title>");
        writeStylesheet();
        writer.println("</head>");
    }

    protected void writeStylesheet() {
        writer.print("<style type=\"text/css\">");
        writer.print("table {margin-bottom:10px;border-collapse:collapse;empty-cells:show;text-align:center} ");
        writer.print("th,td {border:1px solid #BEBEBE;padding:.25em .5em} ");
        writer.print("th {vertical-align:middle;background-color:#FFDAB9}} ");
        writer.print("td {vertical-align:middle;text-align:center} ");
//        writer.print("table a {font-weight:bold} ");
        writer.print(".stripe td {background-color: #FFEFDB} ");  // 换行变色
        writer.print("td.attn {background: #FF6A6A} ");  // foxmail中标红样式
        writer.print(".num {text-align:center} ");
//        writer.print(".passedodd td {background-color: #3F3} ");
//        writer.print(".passedeven td {background-color: #0A0} ");
//        writer.print(".skippedodd td {background-color: #DDD} ");
//        writer.print(".skippedeven td {background-color: #CCC} ");
        writer.print(".failedodd td,.attn {background-color: #FF6A6A} ");
        writer.print(".failedeven td,.stripe .attn {background-color: #FF6A6A} ");
        writer.print(".stacktrace {white-space:pre;font-family:monospace} ");
        writer.print(".totop {font-size:85%;text-align:center;border-bottom:2px solid #000} ");
        writer.print(".invisible {display:none} ");
        writer.println("</style>");
    }

    protected void writeBody() {
        writer.println("<body>");
        writeSuiteSummary(); // 功能模块维度概览
        writer.println("</body>");
    }

    protected void writeDocumentEnd() {
        writer.println("</html>");
    }

    protected void writeSuiteSummary() {
        NumberFormat integerFormat = NumberFormat.getIntegerInstance();
        NumberFormat decimalFormat = NumberFormat.getNumberInstance();

        int totalPassedTests = 0;
        int totalSkippedTests = 0;
        int totalFailedTests = 0;
        int totalRetriedTests = 0;
        long totalDuration = 0;

        writer.println("<table>");
        writer.print("<tr>");
        writer.print("<th>功能</th>");
        writer.print("<th>总数</th>");
        writer.print("<th>成功</th>");
        writer.print("<th>失败</th>");
        writer.print("<th>跳过</th>");
        writer.print("<th>重试</th>");
        writer.print("<th>耗时(s)</th>");
//        writer.print("<th>Included Groups</th>");
        writer.print("<th>包含组</th>");
//        writer.print("<th>Excluded Groups</th>");
        writer.print("<th>排除组</th>");
        writer.println("</tr>");

        int testIndex = 0;
        for (SuiteResult suiteResult : suiteResults) {

            // 没有用例的suite标题不显示
            if (suiteResult.getTestResults().size() < 1) {
                continue;
            }
            // 输出suite名称
            writer.print("<tr><th colspan=\"9\">");
            writer.print(Utils.escapeHtml(suiteResult.getSuiteName()));
            writer.println("</th></tr>");

            for (TestResult testResult : suiteResult.getTestResults()) {
                int passedTests = testResult.getPassedTestCount();
                int skippedTests = testResult.getSkippedTestCount();
                int failedTests = testResult.getFailedTestCount();
                int retriedTests = testResult.getRetriedTestCount();
                int totalTests = passedTests + skippedTests + failedTests + retriedTests;

                // 时间换算为秒,不足一秒按一秒算
                long duration = testResult.getDuration();
                duration = duration / 1000 < 1 ? 1 : duration / 1000;

                writer.print("<tr");
                if ((testIndex % 2) == 1) {
                    writer.print(" class=\"stripe\"");
                }
                writer.print(">");

                buffer.setLength(0);
                writeTableData(
                        buffer.append(Utils.escapeHtml(testResult.getTestName())).toString());
                writeTableData(integerFormat.format(totalTests), "num");
                writeTableData(integerFormat.format(passedTests), "num");
                writeTableData(integerFormat.format(failedTests), (failedTests > 0 ? "num attn" : "num"));
                writeTableData(integerFormat.format(skippedTests), (skippedTests > 0 ? "num attn" : "num"));
                writeTableData(integerFormat.format(retriedTests), (retriedTests > 0 ? "num attn" : "num"));
                writeTableData(decimalFormat.format(duration), "num");
                writeTableData(testResult.getIncludedGroups());
                writeTableData(testResult.getExcludedGroups());

                writer.println("</tr>");

                totalPassedTests += passedTests;
                totalSkippedTests += skippedTests;
                totalFailedTests += failedTests;
                totalRetriedTests += retriedTests;
                totalDuration += duration;

                testIndex++;
            }
            boolean testsInParallel = ParallelMode.TESTS.equals(suiteResult.getParallelMode());
            if (testsInParallel) {
                Optional<TestResult> maxValue = suiteResult.testResults.stream()
                        .max(Comparator.comparing(TestResult::getDuration));
                if (maxValue.isPresent()) {
                    totalDuration = Math.max(totalDuration, maxValue.get().duration);
                }
            }
        }

        // Print totals if there was more than one test
        if (testIndex > 1) {
            writer.print("<tr>");
            writer.print("<th>Total</th>");
            int allTotalTests = totalPassedTests + totalFailedTests + totalSkippedTests + totalRetriedTests;
            writeTableHeader(integerFormat.format(allTotalTests), "num");
            writeTableHeader(integerFormat.format(totalPassedTests), "num");
            writeTableHeader(
                    integerFormat.format(totalFailedTests), (totalFailedTests > 0 ? "num attn" : "num"));
            writeTableHeader(
                    integerFormat.format(totalSkippedTests), (totalSkippedTests > 0 ? "num attn" : "num"));
            writeTableHeader(
                    integerFormat.format(totalRetriedTests), (totalRetriedTests > 0 ? "num attn" : "num"));
            writeTableHeader(decimalFormat.format(totalDuration), "num");
            writer.print("<th colspan=\"2\"></th>");
            writer.println("</tr>");
        }

        writer.println("</table>");
    }

    /**
     * Writes a TH element with the specified contents and CSS class names.
     *
     * @param html       the HTML contents
     * @param cssClasses the space-delimited CSS classes or null if there are no classes to apply
     */
    protected void writeTableHeader(String html, String cssClasses) {
        writeTag("th", html, cssClasses);
    }

    /**
     * Writes a TD element with the specified contents.
     *
     * @param html the HTML contents
     */
    protected void writeTableData(String html) {
        writeTableData(html, null);
    }

    /**
     * Writes a TD element with the specified contents and CSS class names.
     *
     * @param html       the HTML contents
     * @param cssClasses the space-delimited CSS classes or null if there are no classes to apply
     */
    protected void writeTableData(String html, String cssClasses) {
        writeTag("td", html, cssClasses);
    }

    /**
     * Writes an arbitrary HTML element with the specified contents and CSS class names.
     *
     * @param tag        the tag name
     * @param html       the HTML contents
     * @param cssClasses the space-delimited CSS classes or null if there are no classes to apply
     */
    protected void writeTag(String tag, String html, String cssClasses) {
        writer.print("<");
        writer.print(tag);
        if (cssClasses != null) {
            writer.print(" class=\"");
            writer.print(cssClasses);
            writer.print("\"");
        }
        writer.print(">");
        writer.print(html);
        writer.print("</");
        writer.print(tag);
        writer.print(">");
    }

    /**
     * Groups {@link TestResult}s by suite.
     */
    protected static class SuiteResult {
        private final String suiteName;
        private final List<TestResult> testResults = Lists.newArrayList();
        private final ParallelMode mode;

        public SuiteResult(ISuite suite) {
            suiteName = suite.getName();
            mode = suite.getXmlSuite().getParallel();
            for (ISuiteResult suiteResult : suite.getResults().values()) {
                testResults.add(new TestResult(suiteResult.getTestContext()));
            }
        }

        public String getSuiteName() {
            return suiteName;
        }

        /**
         * @return the test results (possibly empty)
         */
        public List<TestResult> getTestResults() {
            return testResults;
        }

        public ParallelMode getParallelMode() {
            return mode;
        }
    }

    /**
     * Groups {@link ClassResult}s by test, type (configuration or test), and status.
     */
    protected static class TestResult {
        /**
         * Orders test results by class name and then by method name (in lexicographic order).
         */
        protected static final Comparator<ITestResult> RESULT_COMPARATOR =
                Comparator.comparing((ITestResult o) -> o.getTestClass().getName())
                        .thenComparing(o -> o.getMethod().getMethodName());

        private final String testName;
        private final List<ClassResult> failedConfigurationResults;
        private final List<ClassResult> failedTestResults;
        private final List<ClassResult> skippedConfigurationResults;
        private final List<ClassResult> skippedTestResults;
        private final List<ClassResult> retriedTestResults;
        private final List<ClassResult> passedTestResults;
        private final int failedTestCount;
        private final int retriedTestCount;
        private final int skippedTestCount;
        private final int passedTestCount;
        private final long duration;
        private final String includedGroups;
        private final String excludedGroups;

        public TestResult(ITestContext context) {
            testName = context.getName();

            Set<ITestResult> failedConfigurations = context.getFailedConfigurations().getAllResults();
            Set<ITestResult> failedTests = context.getFailedTests().getAllResults();
            Set<ITestResult> skippedConfigurations = context.getSkippedConfigurations().getAllResults();
            Set<ITestResult> rawSkipped = context.getSkippedTests().getAllResults();
            Set<ITestResult> skippedTests = pruneSkipped(rawSkipped);
            Set<ITestResult> retriedTests = pruneRetried(rawSkipped);

            Set<ITestResult> passedTests = context.getPassedTests().getAllResults();

            failedConfigurationResults = groupResults(failedConfigurations);
            failedTestResults = groupResults(failedTests);
            skippedConfigurationResults = groupResults(skippedConfigurations);
            skippedTestResults = groupResults(skippedTests);
            retriedTestResults = groupResults(retriedTests);
            passedTestResults = groupResults(passedTests);

            failedTestCount = failedTests.size();
            retriedTestCount = retriedTests.size();
            skippedTestCount = skippedTests.size();
            passedTestCount = passedTests.size();

            duration = context.getEndDate().getTime() - context.getStartDate().getTime();

            includedGroups = formatGroups(context.getIncludedGroups());
            excludedGroups = formatGroups(context.getExcludedGroups());
        }

        private static Set<ITestResult> pruneSkipped(Set<ITestResult> results) {
            return results.stream().filter(result -> !result.wasRetried()).collect(Collectors.toSet());
        }

        private static Set<ITestResult> pruneRetried(Set<ITestResult> results) {
            return results.stream().filter(ITestResult::wasRetried).collect(Collectors.toSet());
        }

        /**
         * Groups test results by method and then by class.
         *
         * @param results All test results
         * @return Test result grouped by method and class
         */
        protected List<ClassResult> groupResults(Set<ITestResult> results) {
            List<ClassResult> classResults = Lists.newArrayList();
            if (!results.isEmpty()) {
                List<MethodResult> resultsPerClass = Lists.newArrayList();
                List<ITestResult> resultsPerMethod = Lists.newArrayList();

                List<ITestResult> resultsList = Lists.newArrayList(results);
                resultsList.sort(RESULT_COMPARATOR);
                Iterator<ITestResult> resultsIterator = resultsList.iterator();
                assert resultsIterator.hasNext();

                ITestResult result = resultsIterator.next();
                resultsPerMethod.add(result);

                String previousClassName = result.getTestClass().getName();
                String previousMethodName = result.getMethod().getMethodName();
                while (resultsIterator.hasNext()) {
                    result = resultsIterator.next();

                    String className = result.getTestClass().getName();
                    if (!previousClassName.equals(className)) {
                        // Different class implies different method
                        assert !resultsPerMethod.isEmpty();
                        resultsPerClass.add(new MethodResult(resultsPerMethod));
                        resultsPerMethod = Lists.newArrayList();

                        assert !resultsPerClass.isEmpty();
                        classResults.add(new ClassResult(previousClassName, resultsPerClass));
                        resultsPerClass = Lists.newArrayList();

                        previousClassName = className;
                        previousMethodName = result.getMethod().getMethodName();
                    } else {
                        String methodName = result.getMethod().getMethodName();
                        if (!previousMethodName.equals(methodName)) {
                            assert !resultsPerMethod.isEmpty();
                            resultsPerClass.add(new MethodResult(resultsPerMethod));
                            resultsPerMethod = Lists.newArrayList();

                            previousMethodName = methodName;
                        }
                    }
                    resultsPerMethod.add(result);
                }
                assert !resultsPerMethod.isEmpty();
                resultsPerClass.add(new MethodResult(resultsPerMethod));
                assert !resultsPerClass.isEmpty();
                classResults.add(new ClassResult(previousClassName, resultsPerClass));
            }
            return classResults;
        }

        public String getTestName() {
            return testName;
        }

        public int getFailedTestCount() {
            return failedTestCount;
        }

        public int getSkippedTestCount() {
            return skippedTestCount;
        }

        public int getRetriedTestCount() {
            return retriedTestCount;
        }

        public int getPassedTestCount() {
            return passedTestCount;
        }

        public long getDuration() {
            return duration;
        }

        public String getIncludedGroups() {
            return includedGroups;
        }

        public String getExcludedGroups() {
            return excludedGroups;
        }

        /**
         * Formats an array of groups for display.
         *
         * @param groups The groups
         * @return The String value of the groups
         */
        protected String formatGroups(String[] groups) {
            if (groups.length == 0) {
                return "";
            }

            StringBuilder builder = new StringBuilder();
            builder.append(groups[0]);
            for (int i = 1; i < groups.length; i++) {
                builder.append(", ").append(groups[i]);
            }
            return builder.toString();
        }
    }

    /**
     * Groups {@link MethodResult}s by class.
     */
    protected static class ClassResult {
        private final String className;
        private final List<MethodResult> methodResults;

        /**
         * @param className     the class name
         * @param methodResults the non-null, non-empty {@link MethodResult} list
         */
        public ClassResult(String className, List<MethodResult> methodResults) {
            this.className = className;
            this.methodResults = methodResults;
        }
    }

    /**
     * Groups test results by method.
     */
    protected static class MethodResult {
        private final List<ITestResult> results;

        /**
         * @param results the non-null, non-empty result list
         */
        public MethodResult(List<ITestResult> results) {
            this.results = results;
        }


    }
}

 

配置Listener

    <listeners>
        <!--testng的XML配置文件中添加这些内容-->
        <listener class-name="xxx.xxx.EmailableReporter2"/>
    </listeners>

 

posted @ 2021-01-04 18:09  星瑞  阅读(643)  评论(0编辑  收藏  举报