zoukankan      html  css  js  c++  java
  • 生成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>
  • 相关阅读:
    java操作redis之jedis篇
    实现指定步长循环后移字符串数组算法
    【PAT Advanced Level】1006. Sign In and Sign Out (25)
    银行计算利息
    中国人、美国人、北京人
    网络子系统55_ip协议分片重组_加入ipq
    C#拦截系统消息的方法-Application.AddMessageFilter
    C#实现在Form上截取消息的两种方法
    Geek改变世界
    中国黑客传说:游走在黑暗中的精灵
  • 原文地址:https://www.cnblogs.com/gongxr/p/14231307.html
Copyright © 2011-2022 走看看