testng生成自定義html報告
阿新 • • 發佈:2018-11-04
目錄
testng自帶的報告,有如下幾個問題:
1. 不是很美觀
2.html報告中js是外接的,在整合jenkins和瀏覽器相容性上均存在一些小問題。因此決定自定義一個測試報告。
自定義的報告截圖如下
實現原理
testng對外提供了很多擴充套件介面,其中測試報告的擴充套件介面為IReporter介面
package org.testng;
import java.util.List;
import org.testng.xml.XmlSuite;
/*實現這個介面,並且配置testng監聽器就可以了*/
public interface IReporter extends ITestNGListener {
default void generateReport(List<XmlSuite> var1, List<ISuite> var2, String var3) {
}
}
因此實現該介面,並且在testng框架裡面配置listener即可,關於監聽器的配置,請參照 https://testng.org/doc/documentation-main.html#testng-listeners
本擴充套件程式就是實現該介面,並且自定義html模板,最終通過Velocity渲染出html報告
原始碼展示
定義測試結果類TestResult
用於儲存測試結果
package org.clearfuny.funnytest.interner.reporter; import org.clearfuny.funnytest.util.ExceptionUtil; import java.util.List; public class TestResult { private String testName; //測試方法名 private String className; //測試類名 private String caseName; private String params; //測試用引數 private String description; //測試描述 private List<String> output; //Reporter Output private Throwable throwable; //測試異常原因 private String throwableTrace; private int status; //狀態 private String duration; private boolean success; 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; this.throwableTrace = ExceptionUtil.getStackTrace(throwable); } 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; } public String getThrowableTrace() { return throwableTrace; } public void setThrowableTrace(String throwableTrace) { this.throwableTrace = throwableTrace; } }
定義 TestResultCollection 集合類
testng採用資料驅動,一個測試類可以有多個測試用例集合,每個測試類,應該有個測試結果集
package org.clearfuny.funnytest.interner.reporter;
import org.testng.ITestResult;
import java.util.LinkedList;
import java.util.List;
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;
}
/*===============================[getter && setter]=================================*/
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;
}
}
自定義測試報告模板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> - TestReport</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: 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 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>
<h1>Test Report: </h1>
<h2>彙總</h2>
<table id="summary">
<tr>
<th>START AT</th>
<td colspan="4">${startTime}</td>
</tr>
<tr>
<th>DURATION</th>
<td colspan="4">$DURATION seconds</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>ID</th>
<th>method</th>
<th>Duration</th>
<th>Detail</th>
</tr>
#foreach($testResult in $item.resultList)
<tr id="${result.key}.${testResult.caseName}.${testResult.testName}">
#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.caseName}</td>
<td>$testResult.testName</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
</body>
擴充套件實現IReporter介面
package org.clearfuny.funnytest.interner.reporter;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.apache.velocity.app.VelocityEngine;
import org.testng.*;
import org.testng.xml.XmlSuite;
import java.io.*;
import java.util.*;
public class ReporterListener implements IReporter {
@Override
public void generateReport(List<XmlSuite> xmlSuites, List<ISuite> suites, String outputDirectory) {
Map<String, Object> result = new HashMap<>();
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("SUCCESS", SUCCESS);
context.put("FAILED", FAILED);
context.put("ERROR", ERROR);
context.put("SKIPPED", SKIPPED);
context.put("startTime", ReportUtil.formatDate(startDate.getTime()));
context.put("DURATION", ReportUtil.formatDuration(endDate.getTime()-startDate.getTime()));
context.put("results", collections);
write(context, outputDirectory);
}
private void write(VelocityContext context, String outputDirectory) {
try {
//寫檔案
VelocityEngine ve = new VelocityEngine();
Properties p = new Properties();
p.setProperty("resource.loader", "class");
p.setProperty("class.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
p.setProperty(Velocity.ENCODING_DEFAULT, "utf-8");
p.setProperty(Velocity.INPUT_ENCODING, "utf-8");
p.setProperty(Velocity.OUTPUT_ENCODING, "utf-8");
ve.init(p);
Template t = ve.getTemplate("report.vm");
OutputStream out = new FileOutputStream(new File(outputDirectory+"/report.html"));
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, "utf-8"));
// 轉換輸出
t.merge(context, writer);
writer.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
private void sort(List<ITestResult> list){
Collections.sort(list, new Comparator<ITestResult>() {
@Override
public int compare(ITestResult r1, ITestResult r2) {
if(r1.getStartMillis()>r2.getStartMillis()){
return 1;
}else{
return -1;
}
}
});
}
private LinkedList<ITestResult> listTestResult(IResultMap resultMap){
Set<ITestResult> results = resultMap.getAllResults();
return new LinkedList<ITestResult>(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;
}
private 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(ReportUtil.getParams(t));
testResult.setTestName(t.getName());
testResult.setStatus(t.getStatus());
testResult.setThrowable(t.getThrowable());
long duration = t.getEndMillis() - t.getStartMillis();
testResult.setDuration(ReportUtil.formatDuration(duration));
testResult.setOutput(Reporter.getOutput(t));
return testResult;
}
}
工具類
package org.clearfuny.funnytest.interner.reporter;
import org.testng.ITestContext;
import org.testng.ITestResult;
import org.testng.Reporter;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.*;
public class ReportUtil {
private static final NumberFormat DURATION_FORMAT = new DecimalFormat("#0.000");
private static final NumberFormat PERCENTAGE_FORMAT = new DecimalFormat("#0.00%");
public static String formatDate(long date){
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return formatter.format(date);
}
/**
* 測試消耗時長
* return 秒,保留3位小數
*/
public String getTestDuration(ITestContext context) {
long duration;
duration = context.getEndDate().getTime() - context.getStartDate().getTime();
return formatDuration(duration);
}
public static String formatDuration(long elapsed) {
double seconds = (double) elapsed / 1000;
return DURATION_FORMAT.format(seconds);
}
/**
* 測試通過率
* return 2.22%,保留2位小數
*/
public String formatPercentage(int numerator, int denominator) {
return PERCENTAGE_FORMAT.format(numerator / (double) denominator);
}
/**
* 獲取方法引數,以逗號分隔
*
* @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);
}
/**
* 獲取依賴的方法
*
* @param result
* @return
*/
public String getDependMethods(ITestResult result) {
String[] methods = result.getMethod().getMethodsDependedUpon();
return commaSeparate(Arrays.asList(methods));
}
/**
* 堆疊軌跡,暫不確定怎麼做,放著先
*
* @param throwable
* @return
*/
public String getCause(Throwable throwable) {
StackTraceElement[] stackTrace = throwable.getStackTrace(); //堆疊軌跡
List<String> list = new ArrayList<String>(stackTrace.length);
for (Object o : stackTrace) {
list.add(renderArgument(o));
}
return commaSeparate(list);
}
/**
* 獲取全部日誌輸出資訊
*
* @return
*/
public List<String> getAllOutput() {
return Reporter.getOutput();
}
/**
* 按testresult獲取日誌輸出資訊
*
* @param result
* @return
*/
public List<String> getTestOutput(ITestResult result) {
return Reporter.getOutput(result);
}
/*將object 轉換為String*/
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();
}
}
/*將集合轉換為以逗號分隔的字串*/
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();
}
}