文章目录
- 一、背景
- 二、具体实现
- 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">×</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