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

news2024/12/22 15:59:53

文章目录

  • 一、背景
  • 二、具体实现
    • 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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JavaScript 进阶(一)

一、作用域 1. 局部作用域 &#xff08;1&#xff09;函数作用域 、 &#xff08;2&#xff09;块作用域 2. 全局作用域 3. 作用域链 g 作用域可以访问 f 作用域&#xff08;子访问父&#xff09;&#xff0c;但是 f 作用域&#xff0c;不能访问 g 作用域&#xff08;父…

[数据集][图像分类]杂草分类数据集17509张9类别

数据集格式&#xff1a;仅仅包含jpg图片&#xff0c;每个类别文件夹下面存放着对应图片 图片数量(jpg文件个数)&#xff1a;17509 分类类别数&#xff1a;9 类别名称:["chineseapple","lantana","negatives","parkinsonia","part…

经典面试题---环形链表

1. 环形链表1. - 力扣&#xff08;LeetCode&#xff09; 要解决这道题&#xff0c;我们首先要挖掘出带环的链表与不带环的链表之间的差别。 以此&#xff0c;才能设计出算法来体现这种差别并判断。 二者最突出的不同&#xff0c;就是不带环的链表有尾结点&#xff0c;也就是说…

Springboot打包jar如何后台启动和查看日志?

如何后台启动Spring Boot的fat jar 使用nohup命令启动&#xff1a; 在Linux或Unix系统中&#xff0c;你可以使用nohup命令来启动jar包&#xff0c;以确保即使你关闭了终端或断开了SSH连接&#xff0c;程序仍然可以在后台运行。命令格式如下&#xff1a;nohup java -jar yourapp…

C语言(指针)6

Hi~&#xff01;这里是奋斗的小羊&#xff0c;很荣幸各位能阅读我的文章&#xff0c;诚请评论指点&#xff0c;关注收藏&#xff0c;欢迎欢迎~~ &#x1f4a5;个人主页&#xff1a;小羊在奋斗 &#x1f4a5;所属专栏&#xff1a;C语言 本系列文章为个人学习笔记&#x…

opencv车道偏离系统-代码+原理-人工智能-自动驾驶

车道偏离预警系统&#xff08;Lane Departure Warning System, LDWS&#xff09;是一种主动安全技术&#xff0c;旨在帮助驾驶员避免因无意中偏离车道而引发的事故。从原理到实战应用&#xff0c;其工作流程大致如下&#xff1a; 传感器采集 &#xff1a;系统通常配备有一个或…

智能终端RK3568主板在智慧公交条形屏项目的应用,支持鸿蒙,支持全国产化

基于AIoT-3568A的智慧公交条形屏&#xff0c;可支持公交线路动态展示&#xff0c;语音到站提醒&#xff0c;减少过乘、漏乘的情况&#xff0c;有效提高了公交服务效率和质量&#xff0c;为乘客提供了更舒适、更安全和更方便的出行体验&#xff0c;为城市的发展增添了新的活力。…

升级Microsoft 365后,SAP GUI中无法打开Excel的解决方案

最近&#xff0c;我们遇到了一个棘手的问题&#xff0c;一位客户在升级到Microsoft 365后&#xff0c;无法在SAP GUI中打开Excel。这个问题不仅影响了工作效率&#xff0c;也给用户的日常操作带来了不便。在本文中&#xff0c;我们将探讨问题的成因&#xff0c;并提供一种解决方…

纯福利|手把手教你如何白嫖免费的GPU资源(二)

大家好&#xff0c;我是无界生长。 前段时间写过一篇文章《纯福利&#xff5c;手把手教你如何白嫖免费的GPU资源&#xff08;一&#xff09;》&#xff0c;使用Google Colab提供的免费的GPU资源&#xff0c;今天接着写白嫖GPU资源攻略&#xff0c;可获得“长期免费的CPU实例资源…

Redis:分布式系统

文章目录 认识RedisRedis和MySQLRedis的场景Redis的设计 分布式单机架构应用数据分离架构应用服务集群架构 认识Redis 在开始Redis学习前&#xff0c;要先认识一下Redis Redis的设计&#xff0c;是想要把它当做是一个数据库&#xff0c;一个缓存&#xff0c;或者说是一个消息…

AI时代的网络安全战:以智取胜,守护数字安宁

在数字化浪潮的推动下&#xff0c;我们的生活和工作日益离不开互联网。然而&#xff0c;随着人工智能&#xff08;AI&#xff09;技术的飞速发展&#xff0c;网络安全问题也日益凸显。美国联邦调查局&#xff08;FBI&#xff09;的一则警报如同一记重锤&#xff0c;敲响了我们对…

无尘室手套的选定标准

无尘室手套的选定标准主要包括以下几个方面&#xff1a; 材料选择&#xff1a; 考虑与应用的相容性&#xff0c;例如某些化学品可能对某些材料产生反应。是否存在乳胶过敏也是一个重要的考虑因素。常见的选择包括丁腈手套&#xff0c;它们通常比乳胶手套更耐用且化学稳定性更…

【爬虫】爬取股票历史K线数据写入数据库(三)

前几天有写过两篇&#xff1a; 【爬虫】爬取A股数据写入数据库&#xff08;二&#xff09; 【爬虫】爬取A股数据写入数据库&#xff08;一&#xff09; 现在继续完善&#xff0c;分析及爬取股票的历史K线数据通过ORM形式批量写入数据库。 2024/05&#xff0c;本文主要内容如下…

Vue的学习 —— <vue响应式基础>

目录 前言 正文 单文件组件 什么是单文件组件 单文件组件使用方法 数据绑定 什么是数据绑定 数据绑定的使用方法 响应式数据绑定 响应式数据绑定的使用方法 ref() 函数 reactive()函数 toRef()函数 toRefs()函数 案例练习 前言 Vue.js 以其高效的数据绑定和视图…

Nginx - location中的匹配规则和动态Proxy

文章目录 官网location 规则详解动态Proxy使用多个 if 指令指定不同的 proxy_pass根据参数选择不同的 proxy_pass 官网 https://nginx.org/en/docs/http/ngx_http_core_module.html#location location 规则详解 Nginx的location指令工作原理如下&#xff1a; 位置匹配&#…

【中级软件设计师】上午题3-数据结构(查漏补缺版)

上午题3-数据结构 0 前言1 时间、空间复杂度2 串2.1 串的模式匹配 3 矩阵4 图4.1 邻接矩阵和邻接表 5 查找6 哈希表、7 树7.1 B树 0 前言 因为我之前考研系统地学习过数据结构和操作系统&#xff0c;这两部分的笔记不完整 1 时间、空间复杂度 指数<阶乘<n次方阶 使用队…

Java入门基础学习笔记22——程序流程控制

程序流程控制&#xff1a;控制程序的执行顺序。 程序有哪些执行顺序&#xff1f; 顺序、分支和循环。 分支结构&#xff1a; if、switch 循环&#xff1a; for、while、do-while 顺序结构是程序中最简单最基本的流程控制&#xff0c;没有特定的语法结构&#xff0c;按照代码…

01-02-5

1、单链表中按位置查找 a.原理 通过传递的位置&#xff0c;返回该位置对应的地址&#xff0c;放到主函数定义的指针变量中。 我们认为位置从&#xff1a;有数据的节点开始计数 即如下结构&#xff1a; 查找位置&#xff0c;就是返回该位置对应的空间地址。 b.代码说明 Ⅰ…

深度剖析深度神经网络(DNN):原理、实现与应用

目录 引言 一、DNN基本原理 二、DNN核心算法原理 三、DNN具体操作步骤 四、代码演示 引言 在人工智能和机器学习的浪潮中&#xff0c;深度神经网络&#xff08;Deep Neural Network&#xff0c;简称DNN&#xff09;已经成为了一种非常重要的工具。DNN模仿人脑神经网络的结…

2023年数维杯国际大学生数学建模挑战赛A题复合直升机的建模与优化控制问题解题全过程论文及程序

2023年数维杯国际大学生数学建模挑战赛 A题 复合直升机的建模与优化控制问题 原题再现&#xff1a; 直升机具有垂直起降等飞行能力&#xff0c;广泛应用于侦察、运输等领域。传统直升机的配置导致旋翼叶片在高速飞行过程中受到冲击波的影响&#xff0c;难以稳定飞行。为了在保…