报表信息 失败截图 鼠标放上图片放大 popj代码 ReportTotal /** * 报表信息 * @author liwen406 * @date 2019-09-25 11:02 */ @Data@AllArgsConstructor@NoArgsConstructor@Accessors(chain = true)public class ReportTotal { private
失败截图
鼠标放上图片放大
popj代码
ReportTotal
/** * 报表信息 * @author liwen406 * @date 2019-09-25 11:02 */ @Data @AllArgsConstructor @NoArgsConstructor @Accessors(chain = true) public class ReportTotal { private Integer id; /** * 开始时间 */ private String testUser; /** * 结束时间 */ private String startTime; /** * 结束时间 */ private String endTime; /** * 合计运行时间 */ private String runTime; /** * 运行版本 */ private String runVersion; /** * 系统版本 */ private String sysVersion; /** * 设备型号(手机) */ private String typeInfo; /** * 用例总数 */ private String total; /** * 成功数据 */ private String successTotal; /** * 失败数 */ private String failedTotal; /** * 跳过 */ private String skippedTotal; /** * 错误数 */ private String errorTotal; /** * 执行执行方法 */ private String runMethod; /** * 状态 */ private String status; /** * 详情 */ private String descriptionInfo; /** * 持续时间 */ private String duration; /** * 日志 */ private String detail;
ReportUtil
/** * @author liwen406 * @Title: ReportUtil * @Description: 报表工具类 * @date 2019/9/19 / 17:34 */ @Data @AllArgsConstructor @NoArgsConstructor @Accessors(chain = true) 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(); }
TestResult
/** * @author liwen406 * @Title: TestResult * @Description: 用于存储测试结果 * @date 2019/9/19 / 17:28 */ @Data @AllArgsConstructor @NoArgsConstructor @Accessors(chain = true) public class TestResult { //测试方法名 private String testName; //测试类名 private String className; private String caseName; //测试用参数 private String params; //测试描述 private String description; //Reporter Output private String output; private List<String> twooutparam ; //测试异常原因 private Throwable throwable; private String throwableTrace; //状态 private int status; private String duration; private boolean success;
TestResultCollection
/** * @author liwen406 * @Title: TestResultCollection * @Description: testng采用数据驱动,一个测试类可以有多个测试用例集合,每个测试类,应该有个测试结果集 * @date 2019/9/19 / 17:31 */ @Data @AllArgsConstructor @NoArgsConstructor @Accessors(chain = true) 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; }
监听器代码
ReporterListener.class
import cases.startdemo.utils.SendMailUtils; import cn.hutool.http.HttpRequest; import com.alibaba.fastjson.JSONObject; import com.jd.dhf.api.test.util.LogUtil; import com.jd.fastjson.JSON; import io.appium.java_client.android.AndroidDriver; import io.appium.java_client.android.AndroidElement; import jdth.getsku.dao.imp.ReportUiDaoImpl; import jdth.getsku.pojo.ReportUi; import jdth.getsku.util.Log; import jdth.getsku.util.OperationalCmd; import jdth.getsku.util.WaitUtil; import jdth.global.pagatest.BestRuner; 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.testng.*; import org.testng.annotations.Test; import org.testng.xml.XmlSuite; import java.io.*; import java.nio.charset.StandardCharsets; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.*; /** * @author liwen * @Title: ReporterListener * @Description: 监听类 UI自动化测试报告 * @date 2019/9/19 / 17:33 */ public class ReporterListener implements IReporter, ITestListener { DateFormat dateFormat = new SimpleDateFormat("yyyy_MMdd_hhmmss"); /** * 保存数据 */ ReportUiDaoImpl reportUiDao = new ReportUiDaoImpl(); @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", ReportUtil.formatDate(startDate.getTime()) + "<--->" + ReportUtil.formatDate(endDate.getTime())); context.put("DURATION", ReportUtil.formatDuration(endDate.getTime() - startDate.getTime())); context.put("results", collections); write(context, outputDirectory); //通过post请求 ReportTotal reportTotal = new ReportTotal(); reportTotal.setTestUser(System.getProperty("user.name")); reportTotal.setStartTime(startDate + ""); reportTotal.setEndTime(endDate + ""); reportTotal.setRunTime(ReportUtil.formatDuration(endDate.getTime() - startDate.getTime())); reportTotal.setRunVersion(OperationalCmd.getVersionNameInfo()); reportTotal.setSysVersion(OperationalCmd.getVersionNameInfo()); reportTotal.setTypeInfo(OperationalCmd.getMobileModel()); reportTotal.setTotal(TOTAL + ""); reportTotal.setSuccessTotal(SUCCESS + ""); reportTotal.setFailedTotal(FAILED + ""); reportTotal.setSkippedTotal(SKIPPED + ""); // String s = JSONObject.toJSONString(reportTotal); // HttpRequest.post("http://127.0.0.1:8081/report/inserttotal").body(s).execute().body(); } /** * 输出模板 * * @param context * @param outputDirectory */ private void write(VelocityContext context, String outputDirectory) { String fileDir = ReporterListener.class.getResource("/Template").getPath(); String reslutpath = outputDirectory + "/html/report" + dateFormat.format(new Date()) + ".html"; 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("report.vm"); //输出结果 OutputStream out = new FileOutputStream(new File(reslutpath)); BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8)); // 转换输出 t.merge(context, writer); writer.flush(); } catch (IOException e) { e.printStackTrace(); } /** * 发送邮件 */ // SendMailUtils.sendemali(reslutpath); } 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; } private TestResult toTestResult(ITestResult t) { TestResult testResult = new TestResult(); // Object[] params = t.getParameters(); // testResult.setParams(params + ""); // if (params != null && params.length >= 1) { // String caseId = (String) params[0]; // LogUtil.info("caseid" + caseId); // testResult.setCaseName(caseId); // } else { // testResult.setCaseName("null"); // } testResult.setTestName(t.getMethod().getMethodName()); testResult.setParams(System.getProperty("user.name")); testResult.setClassName(t.getTestClass().getName()); // testResult.setParams(ReportUtil.getParams(t)); //获取注解上面的 testName testResult.setCaseName(t.getMethod().getConstructorOrMethod().getMethod().getAnnotation(Test.class).testName()); 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(ReportUtil.formatDuration(duration)); testResult.setTwooutparam(Reporter.getOutput(t)); List<String> output = Reporter.getOutput(t); StringBuffer stringBuffer = new StringBuffer(); for (String s : output) { stringBuffer.append(s + ","); } testResult.setOutput(stringBuffer.toString()); // String s = JSONObject.toJSONString(testResult); // HttpRequest.post("http://127.0.0.1:8081/report/insertreport").body(s).execute().body(); return testResult; } /** * 每次调用测试@Test之前调用 * * @param result */ @Override public void onTestStart(ITestResult result) { logTestStart(result); } /** * 用例执行结束后,用例执行成功时调用 * * @param result */ @Override public void onTestSuccess(ITestResult result) { ReportUi reportUi = new ReportUi(); //开始时间 reportUi.setStartTime(ReportUtil.formatDate(result.getStartMillis())); reportUi.setStatus("1"); //结束时间 reportUi.setEndtime(ReportUtil.formatDate(result.getEndMillis())); //成功数据 reportUi.setSuccess("成功"); //运行用例类名字 reportUi.setRunMethodName(result.getName()); //描述 reportUi.setDescriptionInfo(result.getMethod().getConstructorOrMethod().getMethod().getAnnotation(Test.class).description()); reportUi.setTypeInfo(result.getMethod().getConstructorOrMethod().getMethod().getAnnotation(Test.class).testName()); LogUtil.info("查看是否获取数据:" + result.getMethod().getConstructorOrMethod().getMethod().getAnnotation(Test.class).testName()); //持续时间 reportUi.setDuration(String.valueOf(result.getEndMillis() - result.getStartMillis()) + "毫秒"); //日志 List<String> output = Reporter.getOutput(result); StringBuffer stringBuffer = new StringBuffer(); for (String s : output) { stringBuffer.append(s + "<br/>"); } reportUi.setDetail(stringBuffer.toString()); reportUiDao.savereport(reportUi); logTestEnd(result, "Success"); } /** * 用例执行结束后,用例执行失败时调用 * 跑fail则截图 获取屏幕截图 * * @param result */ @Override public void onTestFailure(ITestResult result) { WaitUtil.sleep(2000); AndroidDriver<AndroidElement> driver = BestRuner.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 + "_" + dateFormat.format(new Date()) + ".png"; File targetFile = new File(location + "/" + s); LogUtil.info("截图位置:"); Reporter.log("<font color=\"#FF0000\">截图位置</font><br /> " + targetFile.getPath()); LogUtil.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\" />"); ReportUi reportUi = new ReportUi(); //开始时间 reportUi.setStartTime(ReportUtil.formatDate(result.getStartMillis())); //结束时间 reportUi.setEndtime(ReportUtil.formatDate(result.getEndMillis())); reportUi.setStatus("0"); //成功数据 reportUi.setFailed("失败"); //运行用例类名字 reportUi.setRunMethodName(result.getName()); //描述 reportUi.setDescriptionInfo(result.getMethod().getConstructorOrMethod().getMethod().getAnnotation(Test.class).description()); reportUi.setTypeInfo(result.getMethod().getConstructorOrMethod().getMethod().getAnnotation(Test.class).testName()); //持续时间 reportUi.setDuration(String.valueOf(result.getEndMillis() - result.getStartMillis()) + "毫秒"); //日志 List<String> output = Reporter.getOutput(result); StringBuffer stringBuffer = new StringBuffer(); for (String s1 : output) { stringBuffer.append(s1 + "<br/>"); } stringBuffer.append("+截图路径:" + targetFile.getPath()); reportUi.setDetail(stringBuffer.toString()); reportUiDao.savereport(reportUi); } /** * 用例执行结束后,用例执行skip时调用 * * @param result */ @Override public void onTestSkipped(ITestResult result) { ReportUi reportUi = new ReportUi(); //开始时间 reportUi.setStartTime(ReportUtil.formatDate(result.getStartMillis())); //结束时间 reportUi.setEndtime(ReportUtil.formatDate(result.getEndMillis())); reportUi.setStatus("2"); //成功数据 reportUi.setSkipped("跳过"); //运行用例类名字 reportUi.setRunMethodName(result.getName()); //描述 reportUi.setDescriptionInfo(result.getMethod().getConstructorOrMethod().getMethod().getAnnotation(Test.class).description()); reportUi.setTypeInfo(result.getMethod().getConstructorOrMethod().getMethod().getAnnotation(Test.class).testName()); //持续时间 reportUi.setDuration(String.valueOf(result.getEndMillis() - result.getStartMillis()) + "毫秒"); //日志 List<String> output = Reporter.getOutput(result); StringBuffer stringBuffer = new StringBuffer(); for (String s : output) { stringBuffer.append(s + "<br/>"); } reportUi.setDetail(stringBuffer.toString()); reportUiDao.savereport(reportUi); logTestEnd(result, "Skipped"); } /** * 每次方法失败但是已经使用successPercentage进行注释时调用,并且此失败仍保留在请求的成功百分比之内。 * * @param result */ @Override public void onTestFailedButWithinSuccessPercentage(ITestResult result) { logTestEnd(result, "FailedButWithinSuccessPercentage"); } /** * 在测试类被实例化之后调用,并在调用任何配置方法之前调用。 * * @param context */ @Override public void onStart(ITestContext context) { return; } /** * 在所有测试运行之后调用,并且所有的配置方法都被调用 * * @param context */ @Override public void onFinish(ITestContext context) { 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; } }
报告模板
注意存放路径
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> 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> <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.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> </div> </td> </tr> #end </table> #end <a href="#top">Android前端UI自动化</a> </body>