移动端自动化测试工具 Appium 之自定义报告

文章目录

  • 一、背景
  • 二、具体实现
    • 1、保存结果实体
    • 2、工具类
    • 3、自定义报告监听类代码
    • 4、模板代码
      • 4.1、report.vm
      • 4.2、执行xml
  • 三、总结

一、背景

自动化测试用例跑完后报告展示是体现咱们价值的一个地方咱们先看原始报告。
在这里插入图片描述
上面报告虽然麻雀虽小但五脏俱全,但是如果用这个发送报告不是很美观,如果错误没有截图与日志,通过观察testng有需要可以继承的监听,可以自定义报告。

如下图:

在这里插入图片描述

点击log弹出对话框并且记录操作日志。
在这里插入图片描述

二、具体实现

1、保存结果实体

package appout.reporter;

import java.util.List;

/**
 * @author 7DGroup
 * @Title: TestResult
 * @Description: 用于存储测试结果
 * @date 2019/11/21 / 19:04
 */

public class TestResult {
    /**
     * 测试方法名
     */
    private String testName;
    /**
     * 测试类名
     */
    private String className;
    /**
     * 用例名称
     */
    private String caseName;
    /**
     * 测试用参数
     */
    private String params;
    /**
     * 测试描述
     */
    private String description;
    /**
     * 报告输出日志Reporter Output
     */
    private List<String> output;
    /**
     * 测试异常原因
     */
    private Throwable throwable;

    /**
     * 线程信息
     */
    private String throwableTrace;
    /**
     * 状态
     */
    private int status;
    /**
     * 持续时间
     */
    private String duration;
    /**
     * 是否成功
     */
    private boolean success;

    public TestResult() {
    }


    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;
    }

    public String getThrowableTrace() {
        return throwableTrace;
    }

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

    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;
    }

    @Override
    public String toString() {
        return "TestResult{" +
                "testName='" + testName + '\'' +
                ", className='" + className + '\'' +
                ", caseName='" + caseName + '\'' +
                ", params='" + params + '\'' +
                ", description='" + description + '\'' +
                ", output=" + output +
                ", throwable=" + throwable +
                ", throwableTrace='" + throwableTrace + '\'' +
                ", status=" + status +
                ", duration='" + duration + '\'' +
                ", success=" + success +
                '}';
    }
}

2、工具类

package appout.reporter;

import org.testng.ITestResult;
import java.util.LinkedList;
import java.util.List;

/**
 * @author 7DGroup
 * @Title: TestResultCollection
 * @Description: testng采用数据驱动,一个测试类可以有多个测试用例集合,每个测试类,应该有个测试结果集
 * @date 2019/11/21 / 19:01
 */

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;
    }



    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;
    }
}

3、自定义报告监听类代码

package appout.reporter;


import appout.base.DriverBase;
import appout.utils.LogUtil;
import appout.utils.OperationalCmd;
import io.appium.java_client.AppiumDriver;
import org.apache.commons.io.FileUtils;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.apache.velocity.app.VelocityEngine;
import org.openqa.selenium.OutputType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.*;
import org.testng.xml.XmlSuite;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * @author 7DGroup
 * @Title: ReporterListener
 * @Description: 自定义报告监听类
 * @date 2019/11/21 / 18:56
 */

public class ReporterListener implements IReporter, ITestListener {
    private static final Logger log = LoggerFactory.getLogger(DriverBase.class);
    private static final NumberFormat DURATION_FORMAT = new DecimalFormat("#0.000");

    @Override
    public void generateReport(List<XmlSuite> xmlSuites, List<ISuite> suites, String outputDirectory) {
        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("mobileModel", OperationalCmd.getMobileModel());
        context.put("versionName", OperationalCmd.getVersionNameInfo());
        context.put("SUCCESS", SUCCESS);
        context.put("FAILED", FAILED);
        context.put("ERROR", ERROR);
        context.put("SKIPPED", SKIPPED);
        context.put("startTime", ReporterListener.formatDate(startDate.getTime()) + "<--->" + ReporterListener.formatDate(endDate.getTime()));
        context.put("DURATION", ReporterListener.formatDuration(endDate.getTime() - startDate.getTime()));
        context.put("results", collections);
        write(context, outputDirectory);
    }

    /**
     * 输出模板
     *
     * @param context
     * @param outputDirectory
     */
    private void write(VelocityContext context, String outputDirectory) {
        if (!new File(outputDirectory).exists()) {
            new File(outputDirectory).mkdirs();
        }
        //获取报告模板
        File f = new File("");
        String absolutePath = f.getAbsolutePath();
        String fileDir = absolutePath + "/template/";
        String reslutpath = outputDirectory + "/html/report" + ReporterListener.formateDate() + ".html";
        File outfile = new File(reslutpath);
        if (!outfile.exists()) {
            outfile.mkdirs();
        }
        try {
            //写文件
            VelocityEngine ve = new VelocityEngine();
            Properties p = new Properties();
            p.setProperty(VelocityEngine.FILE_RESOURCE_LOADER_PATH, fileDir);
            p.setProperty(Velocity.ENCODING_DEFAULT, "utf-8");
            p.setProperty(Velocity.INPUT_ENCODING, "utf-8");
            ve.init(p);

            Template t = ve.getTemplate("reportnew.vm");
            //输出结果
            OutputStream out = new FileOutputStream(new File(reslutpath));
            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8));
            // 转换输出
            t.merge(context, writer);
            writer.flush();
            log.info("报告位置:" + reslutpath);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 排序规则
     *
     * @param list
     */
    private void sort(List<ITestResult> list) {
        Collections.sort(list, new Comparator<ITestResult>() {
            @Override
            public int compare(ITestResult r1, ITestResult r2) {
                if (r1.getStatus() < r2.getStatus()) {
                    return 1;
                } else {
                    return -1;
                }
            }
        });
    }

    private LinkedList<ITestResult> listTestResult(IResultMap resultMap) {
        Set<ITestResult> results = resultMap.getAllResults();
        return new LinkedList<>(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;
    }

    /**
     * 输出报表解析
     * @param t
     * @return
     */
    private appout.reporter.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(getParams(t));
        testResult.setTestName(t.getName());
        testResult.setDescription(t.getMethod().getDescription());
        testResult.setStatus(t.getStatus());
        //异常
        testResult.setThrowableTrace("class: " + t.getTestClass().getName() + " <br/> method: " + t.getName() + " <br/> error: " + t.getThrowable());
        testResult.setThrowable(t.getThrowable());
        long duration = t.getEndMillis() - t.getStartMillis();
        testResult.setDuration(formatDuration(duration));
        //日志
        testResult.setOutput(Reporter.getOutput(t));
        return testResult;
    }

    /**
     * 每次调用测试@Test之前调用
     *
     * @param result
     */
    @Override
    public void onTestStart(ITestResult result) {
        logTestStart(result);

    }

    /**
     * 用例执行结束后,用例执行成功时调用
     *
     * @param result
     */
    @Override
    public void onTestSuccess(ITestResult result) {
        logTestEnd(result, "Success");
    }

    /**
     * 用例执行结束后,用例执行失败时调用
     * 跑fail则截图 获取屏幕截图
     *
     * @param result
     */

    @Override
    public void onTestFailure(ITestResult result) {

        AppiumDriver driver = DriverBase.getDriver();
        File srcFile = driver.getScreenshotAs(OutputType.FILE);

        File location = new File("./test-output/html/result/screenshots");
        if (!location.exists()) {
            location.mkdirs();
        }
        String dest = result.getMethod().getRealClass().getSimpleName() + "." + result.getMethod().getMethodName();
        String s = dest + "_" + formateDate() + ".png";
        File targetFile =
                new File(location + "/" + s);
        log.info("截图位置:");
        Reporter.log("<font color=\"#FF0000\">截图位置</font><br /> " + targetFile.getPath());
        log.info("------file is ---- " + targetFile.getPath());
        try {
            FileUtils.copyFile(srcFile, targetFile);
        } catch (IOException e) {
            e.printStackTrace();
        }
        logTestEnd(result, "Failed");
        //报告截图后面显示
        Reporter.log("<img  src=\"./result/screenshots/" + s + "\" width=\"64\" height=\"64\" alt=\"***\"  onMouseover=\"this.width=353; this.height=613\" onMouseout=\"this.width=64;this.height=64\" />");
    }

    /**
     * 用例执行结束后,用例执行skip时调用
     *
     * @param result
     */
    @Override
    public void onTestSkipped(ITestResult result) {
        logTestEnd(result, "Skipped");
    }

    /**
     * 每次方法失败但是已经使用successPercentage进行注释时调用,并且此失败仍保留在请求的成功百分比之内。
     *
     * @param result
     */
    @Override
    public void onTestFailedButWithinSuccessPercentage(ITestResult result) {
        LogUtil.fatal(result.getTestName());
        logTestEnd(result, "FailedButWithinSuccessPercentage");
    }

    /**
     * 在测试类被实例化之后调用,并在调用任何配置方法之前调用。
     *
     * @param context
     */
    @Override
    public void onStart(ITestContext context) {
        LogUtil.startTestCase(context.getName());
        return;
    }

    /**
     * 在所有测试运行之后调用,并且所有的配置方法都被调用
     *
     * @param context
     */
    @Override
    public void onFinish(ITestContext context) {
        LogUtil.endTestCase(context.getName());
        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===============", tr.getMethod().getMethodName()), true);
        Reporter.log(String.format("用例描述: %s, 优先级: %s", tr.getMethod().getDescription(), tr.getMethod().getPriority()),
                true);
        return;
    }

    /**
     * 日期格式化
     *
     * @return date
     */
    public static String formateDate() {
        SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
        Calendar cal = Calendar.getInstance();
        Date date = cal.getTime();
        return sf.format(date);
    }

    /**
     * 时间转换
     *
     * @param date
     * @return
     */
    public static String formatDate(long date) {
        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return formatter.format(date);
    }

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

    /**
     * 获取方法参数,以逗号分隔
     *
     * @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);
    }

    /**
     * 将object 转换为String
     * @param argument
     * @return
     */
    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();
        }
    }


    /**
     * 将集合转换为以逗号分隔的字符串
     * @param strings
     * @return
     */
    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();
    }

}

4、模板代码

4.1、report.vm

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" rel="stylesheet">
    <!-- HTML5 shim 和 Respond.js 是为了让 IE8 支持 HTML5 元素和媒体查询(media queries)功能 -->
    <!-- 警告:通过 file:// 协议(就是直接将 html 页面拖拽到浏览器中)访问页面时 Respond.js 不起作用 -->
    <!--[if lt IE 9]>
    <script src="https://cdn.jsdelivr.net/npm/html5shiv@3.7.3/dist/html5shiv.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/respond.js@1.4.2/dest/respond.min.js"></script>
    <![endif]-->
    <title>UI自动</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: #2fff65;
        }

        .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: #2fff65;
        }

        .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>
<br>
<h1 align="center">UI自动化回归报告</h1>

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

    <tr>
        <th>开始与结束时间</th>
        <td colspan="2">${startTime}</td>
        <th>执行时间</th>
        <td colspan="2">$DURATION seconds</td>
    </tr>
    <tr>
        <th>运行版本与系统版本</th>
        <td colspan="2">${versionName}</td>
        <th>设备型号</th>
        <td colspan="2">${mobileModel}</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>Method</th>
            <th>Description</th>
            <th>Duration</th>
            <th>Detail</th>
        </tr>
        #foreach($testResult in $item.resultList)
            <tr>
                #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.testName</td>
                <td>${testResult.description}</td>
                <td>${testResult.duration} seconds</td>
                <td class="detail">
                ##                    <a class="button" href="#popup_log_${testResult.caseName}_${testResult.testName}">log</a>
                    <button type="button" class="btn btn-primary btn-lg" data-toggle="modal"
                            data-target="#popup_log_${testResult.caseName}_${testResult.testName}">
                        log
                    </button>
                    <!-- 日志模态框 -->
                    <div class="modal fade" id="popup_log_${testResult.caseName}_${testResult.testName}" tabindex="-1"
                         role="dialog" aria-labelledby="myModalLabel">
                        <div class="modal-dialog" role="document">
                            <div class="modal-content">
                                <div class="modal-header">
                                    <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
                                            aria-hidden="true">&times;</span></button>
                                    <h4 class="modal-title" id="myModalLabel">用例操作步骤</h4>
                                </div>
                                <div class="modal-body">
                                    <div style="overflow: auto">
                                        <table>
                                            <tr>
                                                <th>日志</th>
                                                <td>
                                                    #foreach($msg in $testResult.twooutparam)
                                                        <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 class="modal-footer">
                                    <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
                                </div>
                            </div>
                        </div>
                    </div>

                </td>
            </tr>
        #end
    </table>
    #end
<a href="#top">Android前端UI自动化</a>
<!-- jQuery (Bootstrap 的所有 JavaScript 插件都依赖 jQuery,所以必须放在前边) -->
<script src="https://cdn.jsdelivr.net/npm/jquery@1.12.4/dist/jquery.min.js"></script>
<!-- 加载 Bootstrap 的所有 JavaScript 插件。你也可以根据需要只加载单个插件。 -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/js/bootstrap.min.js"></script>
</body>

注意:
report.vm存放路径,否则路径不对会找不到

4.2、执行xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >
<suite name="UI自动化" parallel="tests" thread-count="1">
    <listeners>
        <listener class-name="appout.reporter.ReporterListener"></listener>
    </listeners>
    <test name="M6TGLMA721108530">
        <parameter name="udid" value="M6TGLMA721108530"/>
        <parameter name="port" value="4723"/>
        <classes>
            <class name="appout.appcase.LoginTest"/>
        </classes>
    </test>
</suite>

三、总结

只要通过上面代码就能自定义自己的报告,希望给大家一点帮助,其实这个模板只有改下就能成为接口测试报告。

相关代码:

  • https://github.com/zuozewei/blog-example/tree/master/auto-test/comsevenday

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/623114.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

更适合户外使用的开放式耳机,佩戴舒适音质悦耳,虹觅HOLME NEO体验

随着气温的逐渐升高&#xff0c;不管是在室内工作娱乐&#xff0c;还是到户外运动健身&#xff0c;戴上一款合适的耳机都会帮我们隔绝燥热与烦闷&#xff0c;享受音乐与生活。现在市面上的耳机类型特别多&#xff0c;我很喜欢那种分体式的开放耳机&#xff0c;感觉这种耳机设计…

C语言——模拟实现库函数atoi

1. atoi atoi&#xff1a;将字符串转换为整数。 1. 头文件 <string.h> 2. 声明&#xff1a; 1. 如果字符串开始有空格&#xff0c;直接跳过。 2. - 会影响打印的数字的正负。 3. 只打印数字字符&#xff0c;遇到非数字字符就停止。 4. 如果转换之后的数字大于 …

商城小程序系统与C#.net商城小程序系统源码_OctShop

在移动互联网与电商的时代&#xff0c;商城小程序系统已经成为了众多企业和商家开展电商业务的重要工具。OctShop将以商城小程序系统、C#商城小程序系统源码和.net 商城小程序系统为主题&#xff0c;探讨这些系统的特点和优势。 一、商城小程序系统是什么 商城小程序系统是一种…

嵌入式STM32中I2C控制器外设详解

STM32中的I2C外设主要负责IIC协议与外界进行通信,就像USART外设一样,我们在学习的过程中,需要抓住I2C应用的重点。 STM32在使用I2C协议时,可以通过两种方式, 一是软件模拟协议 意思是使用CPU直接控制通讯引脚的电平,产生出符合通讯协议标准的逻辑。例如,像点亮LED那样…

HTML静态网页成品作业(HTML+CSS)——动漫哆啦A梦网页(3个页面)

&#x1f389;不定期分享源码&#xff0c;关注不丢失哦 文章目录 一、作品介绍二、作品演示三、代码目录四、网站代码HTML部分代码 五、源码获取 一、作品介绍 &#x1f3f7;️本套采用HTMLCSS&#xff0c;未使用Javacsript代码&#xff0c;共有3个页面。 二、作品演示 三、代…

java多线程 线程交替执行(同步)的各种实现方案

目录 java多线程 线程交替执行&#xff08;同步&#xff09;的各种实现方案需求 指定具体执行顺序实现一&#xff1a;wait_notify机制 Thread标志位实现二&#xff1a;lock_condition机制 Thread标志位实现三&#xff1a;semaphore信号量 不指定具体执行顺序&#xff0c;只交…

AlphaFold 3:开启生物医药新革命

AlphaFold 3简介 DeepMind与Isomorphic Labs联合发布了AlphaFold 3&#xff0c;这是一个可以更准确预测蛋白质和其他生物分子结构及其相互作用的AI模型&#xff0c;标志着生物医学研究的新革命 AlphaFold 3&#xff0c;这款由DeepMind与Isomorphic Lab联手推出的最新人工智能…

HIVE卡口流量需求分析

HIVE卡口流量需求分析 目录 HIVE卡口流量需求分析 1.创建表格 插入数据 2.需求 3.总结&#xff1a; 1.创建表格 插入数据 CREATE TABLE learn3.veh_pass( id STRING COMMENT "卡口编号", pass_time STRING COMMENT "进过时间", pass_num int COMMENT …

Python---Pandas万字总结(1)

Pandas基础-1 Pandas 是 一个强大的分析结构化数据的工具集。Pandas 以 NumPy 为基础&#xff08;实现数据存储和运算&#xff09;&#xff0c;提供了专门用于数据分析的类型、方法和函数&#xff0c;对数据分析和数据挖掘提供了很好的支持&#xff1b;同时 pandas 还可以跟数…

微软: 用于文本到语音合成(TTS)的语言模型方法VALL-E

微软引入了一种用于文本到语音合成(TTS)的语言模型方法。具体而言,微软使用从现成的神经音频编解码器模型中得到的离散编码训练了一个神经编解码器语言模型(称为VALL-E),并将TTS视为条件语言建模任务,而不是像之前的工作那样进行连续信号回归。在预训练阶段,微软将TTS训练数据扩…

JavaSE——集合框架一(2/7)-Collection集合的遍历方式-迭代器、增强for循环、Lambda、案例

目录 Collection的遍历方式 迭代器 增强for循环&#xff08;foreach&#xff09; Lambda表达式遍历集合 案例 需求与分析 代码部分 运行结果 Collection的遍历方式 迭代器 选代器是用来遍历集合的专用方式&#xff08;数组没有选代器&#xff09;&#xff0c;在Java中…

git-将本地项目上传到远程仓库

在gitee中新建一个远程仓库。 填写对应内容。 打开你想上传的文件夹&#xff0c;比如我想上传yuanshen 右击&#xff0c;打开git bash 输入git init初始化仓库 git init 添加项目所有文件 git add . 将添加的文件提交到本地仓库&#xff08;提交说明必填&#xff09; git …

怎么获得公网IP?

什么是公网IP 在计算机网络中&#xff0c;公网IP&#xff08;Internet Protocol&#xff09;是指可以直接被互联网访问和通信的IP地址。相对应的&#xff0c;私网IP则是在局域网内使用的IP地址&#xff0c;无法直接被互联网访问。获得公网IP对于一些特定的网络需求非常重要&am…

主机扫漏:Apache Tomcat 环境问题漏洞(CVE-2023-46589)

文章目录 引言I 修复此安全问题see also引言 Apache Tomcat存在环境问题漏洞,该漏洞源于存在不正确的输入验证漏洞,可能会导致将单个请求视为多个请求,从而在反向代理后面出现请求走私。 Tomcat did not correctly parse HTTP trailer headers. A specially crafted traile…

【LAMMPS学习】八、基础知识(6.5)PyLammps 教程

8. 基础知识 此部分描述了如何使用 LAMMPS 为用户和开发人员执行各种任务。术语表页面还列出了 MD 术语&#xff0c;以及相应 LAMMPS 手册页的链接。 LAMMPS 源代码分发的 examples 目录中包含的示例输入脚本以及示例脚本页面上突出显示的示例输入脚本还展示了如何设置和运行各…

Poetry Camera照相机将照片转换成诗歌并打印出来;吴恩达新课程深入了解Mistral;科学研究AI小助手data-to-paper

✨ 1: Poetry Camera 将拍摄的照片转换成诗歌并打印出来 Poetry Camera——一个能够把它所见之物转化成诗歌并打印出来的相机。你在一个美丽的公园&#xff0c;或者是一个充满故事的老街道。只要用Poetry Camera拍下这一刻&#xff0c;它就能立刻给你一首关于这个场景的诗。 …

(项目)-KDE巡检报告(模板

金山云于12月26日对建行共计【30】个KDE集群,合计【198】台服务器进行了巡检服务。共发现系统风险【135】条,服务风险【1912】条,服务配置风险【368】条。 一、系统风险 1、风险分析(图片+描述) (1)磁盘使用率高 问题描述多个集群的多台服务器磁盘使用率较高,远超过…

ModuleSim 仿真找不到模块 module is not defined

提示如下&#xff1a; # vsim -t 1ps -L altera_ver -L lpm_ver -L sgate_ver -L altera_mf_ver -L altera_lnsim_ver -L cycloneive_ver -L rtl_work -L work -voptargs""acc"" pulse_generator_tb # Start time: 14:26:25 on May 10,2024 # ** Note: (…

图的概念、性质和存储与简单遍历

前置知识&#xff1a;树的基本概念及性质 为了保证学习效果&#xff0c;请保证已经掌握前置知识之后&#xff0c;再来学习本章节&#xff01;如果在阅读中遇到困难&#xff0c;也可以回到前面章节查阅。 学习目标 掌握图的基本概念掌握图的一些性质 图的概念 基本概念 图 (…

绝地求生:经典艾伦格即将回归!绝地求生艾伦格进化史

29.2版本经典艾伦格地图将会回归&#xff0c;让我回顾一下艾伦格地图的改动历史吧&#xff01; 回归时间 2016年早期A测 A测 4.1版本&#xff1a;艾伦格-新视界 主要区域变动 Military Base(军事基地) Military Base Mylta Power&#xff08;大电&#xff09; Mylta Power …