背景
自动化测试用例跑完后报告展示是体现咱们价值的一个地方咱们先看原始报告:
上面报告虽然麻雀虽小但五脏俱全,但是如果用这个发送报告不是很美观,如果错误没有截图与日志,通过观察testng有需要可以继承的监听,可以自定义报告;
自定义报告展示
点击log弹出对话框并且记录操作日志
保存结果实体
1. import java.util.List;
2.
3. /**
4. * @author liwen
5. * @Title: TestResult
6. * @Description: 用于存储测试结果
7. * @date 2019/11/23 / 17:28
8. */
9. public class TestResult {
10. /**
11. * 测试方法名
12. */
13. private String testName;
14. /**
15. * 测试类名
16. */
17. private String className;
18. /**
19. * 用例名称
20. */
21. private String caseName;
22. /**
23. * 测试用参数
24. */
25. private String params;
26. /**
27. * 测试描述
28. */
29. private String description;
30. /**
31. * 报告输出日志Reporter Output
32. */
33. private List<String> output;
34. /**
35. * 测试异常原因
36. */
37. private Throwable throwable;
38.
39. /**
40. * 线程信息
41. */
42. private String throwableTrace;
43. /**
44. * 状态
45. */
46. private int status;
47. /**
48. * 持续时间
49. */
50. private String duration;
51. /**
52. * 是否成功
53. */
54. private boolean success;
55. //省略get/set
工具类
1. import org.testng.ITestResult;
2. import java.util.LinkedList;
3. import java.util.List;
4.
5. /**
6. * @author liwen
7. * @Title: TestResultCollection
8. * @Description: testng采用数据驱动,一个测试类可以有多个测试用例集合,每个测试类,应该有个测试结果集
9. * @date 2019/11/21 / 19:01
10. */
11. public class TestResultCollection {
12. private int totalSize = 0;
13.
14. private int successSize = 0;
15.
16. private int failedSize = 0;
17.
18. private int errorSize = 0;
19.
20. private int skippedSize = 0;
21. private List<TestResult> resultList;
22.
23. public void addTestResult(TestResult result) {
24. if (resultList == null) {
25. resultList = new LinkedList<>();
26. }
27. resultList.add(result);
28.
29. switch (result.getStatus()) {
30. case ITestResult.FAILURE:
31. failedSize += 1;
32. break;
33. case ITestResult.SUCCESS:
34. successSize += 1;
35. break;
36. case ITestResult.SKIP:
37. skippedSize += 1;
38. break;
39. }
40.
41. totalSize += 1;
42. }
43. //省略get/set
ReporterListener代码
1. /**
2. * @author liwen
3. * @Title: ReporterListener
4. * @Description: 自定义报告监听类
5. * @date 2019/11/21 / 18:56
6. */
7. public class ReporterListener implements IReporter, ITestListener {
8. private static final Logger log = LoggerFactory.getLogger(DriverBase.class);
9. private static final NumberFormat DURATION_FORMAT = new DecimalFormat("#0.000");
10.
11. @Override
12. public void generateReport(List<XmlSuite> xmlSuites, List<ISuite> suites, String outputDirectory) {
13. List<ITestResult> list = new LinkedList<>();
14. Date startDate = new Date();
15. Date endDate = new Date();
16.
17. int TOTAL = 0;
18. int SUCCESS = 1;
19. int FAILED = 0;
20. int ERROR = 0;
21. int SKIPPED = 0;
22. for (ISuite suite : suites) {
23. Map<String, ISuiteResult> suiteResults = suite.getResults();
24. for (ISuiteResult suiteResult : suiteResults.values()) {
25. ITestContext testContext = suiteResult.getTestContext();
26.
27. startDate = startDate.getTime() > testContext.getStartDate().getTime() ? testContext.getStartDate() : startDate;
28.
29. if (endDate == null) {
30. endDate = testContext.getEndDate();
31. } else {
32. endDate = endDate.getTime() < testContext.getEndDate().getTime() ? testContext.getEndDate() : endDate;
33. }
34.
35. IResultMap passedTests = testContext.getPassedTests();
36. IResultMap failedTests = testContext.getFailedTests();
37. IResultMap skippedTests = testContext.getSkippedTests();
38. IResultMap failedConfig = testContext.getFailedConfigurations();
39.
40. SUCCESS += passedTests.size();
41. FAILED += failedTests.size();
42. SKIPPED += skippedTests.size();
43. ERROR += failedConfig.size();
44.
45. list.addAll(this.listTestResult(passedTests));
46. list.addAll(this.listTestResult(failedTests));
47. list.addAll(this.listTestResult(skippedTests));
48. list.addAll(this.listTestResult(failedConfig));
49. }
50. }
51. /* 计算总数 */
52. TOTAL = SUCCESS + FAILED + SKIPPED + ERROR;
53.
54. this.sort(list);
55. Map<String, TestResultCollection> collections = this.parse(list);
56. VelocityContext context = new VelocityContext();
57.
58. context.put("TOTAL", TOTAL);
59. context.put("mobileModel", OperationalCmd.getMobileModel());
60. context.put("versionName", OperationalCmd.getVersionNameInfo());
61. context.put("SUCCESS", SUCCESS);
62. context.put("FAILED", FAILED);
63. context.put("ERROR", ERROR);
64. context.put("SKIPPED", SKIPPED);
65. context.put("startTime", ReporterListener.formatDate(startDate.getTime()) + "<--->" + ReporterListener.formatDate(endDate.getTime()));
66. context.put("DURATION", ReporterListener.formatDuration(endDate.getTime() - startDate.getTime()));
67. context.put("results", collections);
68. write(context, outputDirectory);
69. }
70.
71. /**
72. * 输出模板
73. *
74. * @param context
75. * @param outputDirectory
76. */
77. private void write(VelocityContext context, String outputDirectory) {
78. if (!new File(outputDirectory).exists()) {
79. new File(outputDirectory).mkdirs();
80. }
81. //获取报告模板
82. File f = new File("");
83. String absolutePath = f.getAbsolutePath();
84. String fileDir = absolutePath + "/template/";
85. String reslutpath = outputDirectory + "/html/report" + ReporterListener.formateDate() + ".html";
86. File outfile = new File(reslutpath);
87. if (!outfile.exists()) {
88. outfile.mkdirs();
89. }
90. try {
91. //写文件
92. VelocityEngine ve = new VelocityEngine();
93. Properties p = new Properties();
94. p.setProperty(VelocityEngine.FILE_RESOURCE_LOADER_PATH, fileDir);
95. p.setProperty(Velocity.ENCODING_DEFAULT, "utf-8");
96. p.setProperty(Velocity.INPUT_ENCODING, "utf-8");
97. ve.init(p);
98.
99. Template t = ve.getTemplate("reportnew.vm");
100. //输出结果
101. OutputStream out = new FileOutputStream(new File(reslutpath));
102. BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8));
103. // 转换输出
104. t.merge(context, writer);
105. writer.flush();
106. log.info("报告位置:" + reslutpath);
107. } catch (IOException e) {
108. e.printStackTrace();
109. }
110. }
111.
112. /**
113. * 排序规则
114. *
115. * @param list
116. */
117. private void sort(List<ITestResult> list) {
118. Collections.sort(list, new Comparator<ITestResult>() {
119. @Override
120. public int compare(ITestResult r1, ITestResult r2) {
121. if (r1.getStatus() < r2.getStatus()) {
122. return 1;
123. } else {
124. return -1;
125. }
126. }
127. });
128. }
129.
130. private LinkedList<ITestResult> listTestResult(IResultMap resultMap) {
131. Set<ITestResult> results = resultMap.getAllResults();
132. return new LinkedList<>(results);
133. }
134.
135. private Map<String, TestResultCollection> parse(List<ITestResult> list) {
136.
137. Map<String, TestResultCollection> collectionMap = new HashMap<>();
138.
139. for (ITestResult t : list) {
140. String className = t.getTestClass().getName();
141. if (collectionMap.containsKey(className)) {
142. TestResultCollection collection = collectionMap.get(className);
143. collection.addTestResult(toTestResult(t));
144.
145. } else {
146. TestResultCollection collection = new TestResultCollection();
147. collection.addTestResult(toTestResult(t));
148. collectionMap.put(className, collection);
149. }
150. }
151.
152. return collectionMap;
153. }
154.
155. /**
156. * 输出报表解析
157. * @param t
158. * @return
159. */
160. private appout.reporter.TestResult toTestResult(ITestResult t) {
161. TestResult testResult = new TestResult();
162. Object[] params = t.getParameters();
163.
164. if (params != null && params.length >= 1) {
165. String caseId = (String) params[0];
166. testResult.setCaseName(caseId);
167. } else {
168. testResult.setCaseName("null");
169. }
170. testResult.setClassName(t.getTestClass().getName());
171. testResult.setParams(getParams(t));
172. testResult.setTestName(t.getName());
173. testResult.setDescription(t.getMethod().getDescription());
174. testResult.setStatus(t.getStatus());
175. //异常
176. testResult.setThrowableTrace("class: " + t.getTestClass().getName() + " <br/> method: " + t.getName() + " <br/> error: " + t.getThrowable());
177. testResult.setThrowable(t.getThrowable());
178. long duration = t.getEndMillis() - t.getStartMillis();
179. testResult.setDuration(formatDuration(duration));
180. //日志
181. testResult.setOutput(Reporter.getOutput(t));
182. return testResult;
183. }
184.
185. /**
186. * 每次调用测试@Test之前调用
187. *
188. * @param result
189. */
190. @Override
191. public void onTestStart(ITestResult result) {
192. logTestStart(result);
193.
194. }
195.
196. /**
197. * 用例执行结束后,用例执行成功时调用
198. *
199. * @param result
200. */
201. @Override
202. public void onTestSuccess(ITestResult result) {
203. logTestEnd(result, "Success");
204. }
205.
206. /**
207. * 用例执行结束后,用例执行失败时调用
208. * 跑fail则截图 获取屏幕截图
209. *
210. * @param result
211. */
212.
213. @Override
214. public void onTestFailure(ITestResult result) {
215.
216. AppiumDriver driver = DriverBase.getDriver();
217. File srcFile = driver.getScreenshotAs(OutputType.FILE);
218.
219. File location = new File("./test-output/html/result/screenshots");
220. if (!location.exists()) {
221. location.mkdirs();
222. }
223. String dest = result.getMethod().getRealClass().getSimpleName() + "." + result.getMethod().getMethodName();
224. String s = dest + "_" + formateDate() + ".png";
225. File targetFile =
226. new File(location + "/" + s);
227. log.info("截图位置:");
228. Reporter.log("<font color=\"#FF0000\">截图位置</font><br /> " + targetFile.getPath());
229. log.info("------file is ---- " + targetFile.getPath());
230. try {
231. FileUtils.copyFile(srcFile, targetFile);
232. } catch (IOException e) {
233. e.printStackTrace();
234. }
235. logTestEnd(result, "Failed");
236. //报告截图后面显示
237. 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\" />");
238. }
239.
240. /**
241. * 用例执行结束后,用例执行skip时调用
242. *
243. * @param result
244. */
245. @Override
246. public void onTestSkipped(ITestResult result) {
247. logTestEnd(result, "Skipped");
248. }
249.
250. /**
251. * 每次方法失败但是已经使用successPercentage进行注释时调用,并且此失败仍保留在请求的成功百分比之内。
252. *
253. * @param result
254. */
255. @Override
256. public void onTestFailedButWithinSuccessPercentage(ITestResult result) {
257. LogUtil.fatal(result.getTestName());
258. logTestEnd(result, "FailedButWithinSuccessPercentage");
259. }
260.
261. /**
262. * 在测试类被实例化之后调用,并在调用任何配置方法之前调用。
263. *
264. * @param context
265. */
266. @Override
267. public void onStart(ITestContext context) {
268. LogUtil.startTestCase(context.getName());
269. return;
270. }
271.
272. /**
273. * 在所有测试运行之后调用,并且所有的配置方法都被调用
274. *
275. * @param context
276. */
277. @Override
278. public void onFinish(ITestContext context) {
279. LogUtil.endTestCase(context.getName());
280. return;
281. }
282.
283. /**
284. * 在用例执行结束时,打印用例的执行结果信息
285. */
286. protected void logTestEnd(ITestResult tr, String result) {
287. Reporter.log(String.format("=============Result: %s=============", result), true);
288.
289. }
290.
291. /**
292. * 在用例开始时,打印用例的一些信息,比如@Test对应的方法名,用例的描述等等
293. */
294. protected void logTestStart(ITestResult tr) {
295. Reporter.log(String.format("=============Run: %s===============", tr.getMethod().getMethodName()), true);
296. Reporter.log(String.format("用例描述: %s, 优先级: %s", tr.getMethod().getDescription(), tr.getMethod().getPriority()),
297. true);
298. return;
299. }
300.
301. /**
302. * 日期格式化
303. *
304. * @return date
305. */
306. public static String formateDate() {
307. SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
308. Calendar cal = Calendar.getInstance();
309. Date date = cal.getTime();
310. return sf.format(date);
311. }
312.
313. /**
314. * 时间转换
315. *
316. * @param date
317. * @return
318. */
319. public static String formatDate(long date) {
320. SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
321. return formatter.format(date);
322. }
323.
324. public static String formatDuration(long elapsed) {
325. double seconds = (double) elapsed / 1000;
326. return DURATION_FORMAT.format(seconds);
327. }
328.
329. /**
330. * 获取方法参数,以逗号分隔
331. *
332. * @param result
333. * @return
334. */
335. public static String getParams(ITestResult result) {
336. Object[] params = result.getParameters();
337. List<String> list = new ArrayList<String>(params.length);
338. for (Object o : params) {
339. list.add(renderArgument(o));
340. }
341. return commaSeparate(list);
342. }
343.
344. /**
345. * 将object 转换为String
346. * @param argument
347. * @return
348. */
349. private static String renderArgument(Object argument) {
350. if (argument == null) {
351. return "null";
352. } else if (argument instanceof String) {
353. return "\"" + argument + "\"";
354. } else if (argument instanceof Character) {
355. return "\'" + argument + "\'";
356. } else {
357. return argument.toString();
358. }
359. }
360.
361.
362. /**
363. * 将集合转换为以逗号分隔的字符串
364. * @param strings
365. * @return
366. */
367. private static String commaSeparate(Collection<String> strings) {
368. StringBuilder buffer = new StringBuilder();
369. Iterator<String> iterator = strings.iterator();
370. while (iterator.hasNext()) {
371. String string = iterator.next();
372. buffer.append(string);
373. if (iterator.hasNext()) {
374. buffer.append(", ");
375. }
376. }
377. return buffer.toString();
378. }
379.
380. }
模板:
css
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:lightgreen}.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:greenyellow}.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 .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%}}
report.vm
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>UI自动</title>
<style>
css在上面
</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>
<div id="popup_log_${testResult.caseName}_${testResult.testName}" class="overlay">
<div class="popup">
<h2>Request and Response data</h2>
<a class="close" href="">×</a>
<div class="content">
<h3>Response:</h3>
<div style="overflow: auto">
<table>
<tr>
<th>日志</th>
<td>
#foreach($msg in $testResult.output)
<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>
</div>
</td>
</tr>
#end
</table>
#end
<a href="#top">Android前端UI自动化</a>
</body>
注意:report.vm存放路径,否则路径不对会找不到
执行xml:
<?xml version="1.0" encoding="UTF-8"?>
<suite name="UI自动化" parallel="tests" thread-count="1">
<listeners>
<listener class-name="myseven.reporter.ReporterListener"/>
</listeners>
<test name="M6TGLMA721108530">
<parameter name="udid" value="M6TGLMA721108530"/>
<parameter name="port" value="5190"/>
<classes>
<class name="autotest.runbase.usecase.SearchReTest"/>
</classes>
</test>
</suite>
总结:
只要通过上面代码就能自定义自己的报告,希望给大家一点帮助,其实这个模板只有改下就能成为接口测试报告。
源码位置:https://github.com/357712148/bodygit.git
送大家一句话
假如真有来世,我愿生生世世为人,只做芸芸众生中的一个,哪怕一生贫困清苦,浪迹天涯,只要能爱恨歌哭,只要能心遂所愿。
——仓央嘉措