此文已由作者范旭斐授权网易云社区发布。
欢迎访问网易云社区,了解更多网易技术产品运营经验。
抛砖引玉
假设我们将testng作为自动化测试框架的选型方案,以下两个问题如何实现:
问题1:如何将每次执行(手动或CI自动构建)后的每一条TestNG的测试结果,包括用例的描述,分组,优先级,执行日志,执行结果等存储到数据库存档?
问题2:在UI自动化测试中,如何实现用例失败自动截屏功能?
TestNG的Listener列表
TestNG提供了一组预定义的Listener Java接口,这些接口全部继承自TestNG的 ITestNGListener接口。用户创建这些接口的实现类,并把它们加入到 TestNG 中,TestNG便会在测试运行的不同时刻调用这些类中的接口方法:
- IExecutionListener 监听TestNG运行的启动和停止。
- IAnnotationTransformer 注解转换器,用于TestNG测试类中的注解。
- ISuiteListener 测试套件监听器,监听测试套件的启动和停止。
- ITestListener 测试运行的监听器。
- IConfigurationListener 监听配置方法相关的接口。
- IMethodInterceptor 用于修改TestNG即将运行的测试方法列表。
- IInvokedMethodListener 测试方法拦截监听,用于获取被TestNG调用的在Method的Before 和After方法监听器。该方法只会被配置和测试方法调用。
- IHookable 若测试类实现了该接口,当@Test方法被发现时,它的run()方法将会被调用来替代@Test方法。这个测试方法通常在IHookCallBack的callback之上调用,比较适用于需要JASS授权的测试类。
- IReporter 实现该接口可以生成一份测试报告。
本文将着重介绍最常用到的两个Listener ITestListener与Ireporter接口。ITestListener
实现ITestListener接口的类在加入TestNG后,会在用例执行期间,测试类加载后,每个测试方法@Test之前前后调用执行。以下为实现了该接口的一个demo类:
import org.testng.ITestContext;
import org.testng.ITestListener;
import org.testng.ITestResult;
import org.testng.Reporter;
public class KaolaTestListener implements ITestListener {
//用例执行结束后,用例执行成功时调用
public void onTestSuccess(ITestResult tr) {
logTestEnd(tr, "Success");
}
//用例执行结束后,用例执行失败时调用
public void onTestFailure(ITestResult tr) {
logTestEnd(tr, "Failed");
}
//用例执行结束后,用例执行skip时调用
public void onTestSkipped(ITestResult tr) {
logTestEnd(tr, "Skipped");
}
//每次方法失败但是已经使用successPercentage进行注释时调用,并且此失败仍保留在请求的成功百分比之内。
public void onTestFailedButWithinSuccessPercentage(ITestResult tr) {
logTestEnd(tr, "FailedButWithinSuccessPercentage");
}
//每次调用测试@Test之前调用
public void onTestStart(ITestResult result) {
logTestStart(result);
}
//在测试类被实例化之后调用,并在调用任何配置方法之前调用。
public void onStart(ITestContext context) {
return;
}
//在所有测试运行之后调用,并且所有的配置方法都被调用
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()), true);
Reporter.log(String.format("用例描述: %s, 优先级: %s", tr.getMethod().getDescription(), tr.getMethod().getPriority()),
true);
return;
}
}
这里写一个简单的demo用例:
@Test(description = "demo用例的示例描述", priority = 0)
public void demo() throws IOException, InterruptedException {
Reporter.log("步骤1:调用接口", true);
HttpGetAPI getAPI = new HttpGetAPI();
getAPI.setHost("127.0.0.1");
getAPI.setPort("9999");
getAPI.setPath("/api/sayHello");
getAPI.getUriParams().put("name", "luck");
getAPI.sendRequest(null);
Reporter.log("步骤2:接口调用结果校验", true);
getAPI.verifyResponseStatus(200, "");
}
将示例的 KaolaTestListener类加入到 TestNG 中,运行一个demo用例后的执行效果如下:
可以看到用例运行后的日志中,在用例开始前与结束后添加了我们写的借助 org.testng.Reporter打印的一些日志信息。
IReporter
实现IReporter接口加入TestNG后,在每次测试执行完后被调用执行,可以获取所有执行的test suite,testcase的一些信息,可以用以生成一份测试报告。以下基于 extentreports实现IReporter接口,自定义扩展的测试报告为例:
extentreports是 aventstack公司开发的一个报告工具,其提供了开源版本,支持java及.net项目的测试报告生成。extentreports的一些介绍:http://extentreports.com/docs/versions/3/java/
extentreports的pom依赖:
<dependency>
<groupId>com.aventstack</groupId>
<artifactId>extentreports</artifactId>
<version>3.1.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/aopalliance/aopalliance -->
<dependency>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
<version>1.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/aspectj/aspectjweaver -->
<dependency>
<groupId>aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.5.4</version>
</dependency>
基于extentreports实现的IReporter接口:
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Map;
import org.testng.IReporter;
import org.testng.IResultMap;
import org.testng.ISuite;
import org.testng.ISuiteResult;
import org.testng.ITestContext;
import org.testng.ITestResult;
import org.testng.Reporter;
import org.testng.xml.XmlSuite;
import com.netease.kaola.onlinetest.test.common.ExtentReportsContext;
import com.alibaba.fastjson.JSONObject;
import com.aventstack.extentreports.ExtentTest;
import com.aventstack.extentreports.Status;
import org.apache.commons.lang3.StringUtils;
public class ExtentTestNGReporter implements IReporter {
List groups = new ArrayList();
public void generateReport(List xmlSuites, List suites, String outputDirectory) {
for (ISuite suite : suites) {
Map result = suite.getResults();
for (ISuiteResult r : result.values()) {
ITestContext context = r.getTestContext();
ExtentTest parent = ExtentReportsContext.getInstance().createTest(context.getSuite().getName());
ExtentReportsContext.parentTest.set(parent);
buildTestNodes(context.getFailedTests(), Status.FAIL);
buildTestNodes(context.getSkippedTests(), Status.SKIP);
buildTestNodes(context.getPassedTests(), Status.PASS);
for (String group : groups) {
ExtentReportsContext.parentTest.get().assignCategory(group);
ExtentReportsContext.getInstance().flush();
}
groups.clear();
}
}
for (String s : org.testng.Reporter.getOutput()) {
ExtentReportsContext.getInstance().setTestRunnerOutput(s);
}
}
private void buildTestNodes(IResultMap tests, Status status) {
if (tests.size() > 0) {
for (ITestResult result : tests.getAllResults()) {
ExtentTest child = ExtentReportsContext.parentTest.get().createNode(result.getMethod().getMethodName());
ExtentReportsContext.test.set(child);
String groupsStr = "";
for (String group : result.getMethod().getGroups()) {
if (!groups.contains(group)) {
groups.add(group);
}
if (!StringUtils.isEmpty(groupsStr)) {
groupsStr += "|";
}
groupsStr += group;
ExtentReportsContext.test.get().assignCategory(group);
}
if (!StringUtils.isEmpty(result.getMethod().getDescription())) {
ExtentReportsContext.test.get().log(Status.PASS,
String.format("用例描述:%s Priority:%s 分组:%s", result.getMethod().getDescription(),
Integer.toString(result.getMethod().getPriority()), groupsStr));
}
if (result.getParameters().length > 0) {
for (int i = 0; i < result.getParameters().length; i++) {
ExtentReportsContext.test.get().log(Status.PASS, "用例参数列表:");
ExtentReportsContext.test.get().log(Status.PASS, String.format("第%d个参数:", i + 1));
ExtentReportsContext.test.get().log(Status.PASS,
JSONObject.toJSONString(result.getParameters()[i]));
}
}
ExtentReportsContext.test.get().log(Status.PASS,
String.format("=============Run: %s===============", result.getMethod()));
List outputs = Reporter.getOutput(result);
if (outputs != null) {
for (String output : outputs) {
ExtentReportsContext.test.get().log(Status.PASS, output);
}
}
if (result.getThrowable() != null) {
ExtentReportsContext.test.get().log(status, result.getThrowable());
}
ExtentReportsContext.test.get().getModel().setStartTime(getTime(result.getStartMillis()));
ExtentReportsContext.test.get().getModel().setEndTime(getTime(result.getEndMillis()));
}
ExtentReportsContext.getInstance().flush();
}
}
private Date getTime(long millis) {
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(millis);
return calendar.getTime();
}
}
import com.aventstack.extentreports.ExtentReports;
import com.aventstack.extentreports.ExtentTest;
import com.aventstack.extentreports.ResourceCDN;
import com.aventstack.extentreports.reporter.ExtentHtmlReporter;
import com.aventstack.extentreports.reporter.configuration.ChartLocation;
public class ExtentReportsContext {
private static ExtentReports extent;
public static ThreadLocal parentTest = new ThreadLocal();
public static ThreadLocal test = new ThreadLocal();
public static ExtentReports getInstance() {
if (extent == null)
createInstance("接口测试报告.html");
return extent;
}
public static ExtentReports createInstance(String fileName) {
ExtentHtmlReporter htmlReporter = new ExtentHtmlReporter(fileName);
htmlReporter.config().setTestViewChartLocation(ChartLocation.BOTTOM);
htmlReporter.config().setChartVisibilityOnOpen(true);
// htmlReporter.config().setTheme(Theme.STANDARD);
htmlReporter.config().setResourceCDN(ResourceCDN.EXTENTREPORTS);
htmlReporter.config().setDocumentTitle(fileName);
htmlReporter.config().setEncoding("utf-8");
htmlReporter.config().setReportName(fileName);
extent = new ExtentReports();
extent.attachReporter(htmlReporter);
return extent;
}
}
这里 ExtentReportsContext与 ExtentTestNGReporter的编写主要参考extentreports的官方文档:http://extentreports.com/docs/versions/3/java/#testng-ireporter
将示例的ExtentTestNGReporter类加入到 TestNG 中,运行一个demo用例后,可以在target目录下查看生成的接口测试报告 “接口测试报告.html”:
可以看到extentreports比原生的testng生成的测试报告界面更友好,内容也更丰富。因为我们在ExtentTestNGReporter用到了org.testng.Reporter. getOutput(ITestResult tr )方法,所以所有调用到org.testng.Reporter. log()方法的地方,都可以在测试报告中展示出来。
如何将实现了ITestNGListener接口加入到TestNG中
这里有三个地方可以为TestNG配置Listener
- pom文件中maven-surefire-plugin插件,maven-surefire-plugin插件集成了TestNG&JUnit,任何实现了ITestNGListener的接口的类都可以配置进来。
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${version.maven.plugins}</version>
<configuration>
<useSystemClassLoader>true</useSystemClassLoader>
<testFailureIgnore>true</testFailureIgnore>
<parallel>false</parallel>
<forkMode>once</forkMode>
<suiteXmlFiles>
<suiteXmlFile>src/main/resources/testng/${run}-${runtype}-testng.xml</suiteXmlFile>
</suiteXmlFiles>
<properties>
<property>
<name>usedefaultlisteners</name>
<value>false</value>
</property>
<property>
<name>listener</name>
<value>org.uncommons.reportng.HTMLReporter,org.uncommons.reportng.JUnitXMLReporter,com.netease.kaola.onlinetest.test.common.ExtentTestNGReporter</value>
</property>
</properties>
<workingDirectory>target/</workingDirectory>
</configuration>
</plugin>
testng的xml配置文件中
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="dubbok_bvt_testsuite" verbose="1" parallel="false">
<test name="dubbok_bvt_testsuite">
<groups>
<run>
<include name="dubbok" />
</run>
</groups>
<packages>
<package name="com.netease.kaola.onlinetest.test.bvt.dubbok.*" />
</packages>
</test>
<listeners>
<listener class-name="com.netease.kaola.onlinetest.test.common.ExtentTestNGReporter" />
<listener class-name="com.netease.kaola.onlinetest.test.common.KaolaTestListener" />
</listeners>
</suite>
测试类添加注解标签
package com.netease.kaola.onlinetest.test.base;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.testng.AbstractTestNGSpringContextTests;
import org.testng.annotations.Listeners;
import com.netease.kaola.onlinetest.test.common.KaolaTestListener;
@Listeners({ KaolaTestListener.class })
@ContextConfiguration(locations = { "classpath:application.xml" })
public abstract class BaseTest extends AbstractTestNGSpringContextTests {
}
回到开始的问题
假设我们将testng作为自动化测试框架的选型方案,以下两个问题如何实现:
问题1:如何将每次执行(手动或CI自动构建)后的每一条TestNG的测试结果,包括用例的描述,分组,优先级,执行日志,执行结果等存储到数据库存档?
答案:实现一个TestNG的IReporter接口,在自定义的Reporter类中,将需要的一些用例信息收集出来,保存在数据库中,最后将自定义的Reporter类配置在TestNG中。
问题2:在UI自动化测试中,如何实现用例失败自动截屏功能?
答案:实现一个TestNG的 ITestListener 接口,在自定义的TestListener类的 onTestFailure()方法中实现截屏逻辑 ,最后将自定义的 TestListener 类配置在TestNG中。
免费领取验证码、内容安全、短信发送、直播点播体验包及云服务器等套餐
更多网易技术、产品、运营经验分享请点击。