testng框架Listener介绍及测试结果的收集

阿凡达2018-08-31 18:26
作者:范旭斐

抛砖引玉
假设我们将testng作为自动化测试框架的选型方案,以下两个问题如何实现:
问题1:如何将每次执行(手动或CI自动构建)后的每一条TestNG的测试结果,包括用例的描述,分组,优先级,执行日志,执行结果等存储到数据库存档?
问题2:在UI自动化测试中,如何实现用例失败自动截屏功能?

TestNGListener列表
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中。

网易云大礼包:https://www.163yun.com/gift

本文来自网易实践者社区,经作者范旭斐授权发布。