testng集成测试框架自定义测试报告
目录
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();
}
}