背景
在我们执行自动化测试脚本的时候,可能会因为某个时刻的网络较差,或者某些客观原因导致用例失败,但是很多时候重跑一下脚本的时候就好了。为了提高脚本执行稳定性,也为了减少我们测试同学排查脚本报错的工作量,需要能让用例失败的时候能够自己自动的重试
需求分析
- 要能够做到失败时自动重试,重试时保留测试参数不变
- 希望是用例级别的重试,而不是整个用例集,因为那样太浪费时间
- 希望可以对于脚本的侵入最小化,因为我们的用例数太多,每个都要改的话工作量太大,要能既支持单个用例,又能针对用例集配置
方案选型(Java方向)
网络请求层面
我们的自动化框架中,所有的网络请求相关,是使用了httpclient包,做了网络请求层的封装。
所以可以使用httpclient的retry机制来做重试
- 重写HttpRequestRetryHandler中的retryRequest方法,其中有个executionCount计数器,来限制重试次数
- 自定义retryRequest中的exception类型,来指定需要重试的异常类型
要注意:
CloseableHttpClient的改造是全局的,要注意有些接口是不是幂等的,是否可以重试,是否有必要重试
private static CloseableHttpClient getCustomHttpClient() {
return HttpClients.custom()
.setSSLContext(sslContext)
.setSSLHostnameVerifier(new NoopHostnameVerifier ())
.setRetryHandler(myRetryHandler)
.build();
}
static HttpRequestRetryHandler myRetryHandler = new HttpRequestRetryHandler() {
@Override
public boolean retryRequest(
IOException exception,
int executionCount,
HttpContext context) {
if (executionCount >= 3) {
// Do not retry if over max retry count
return false;
}
if (exception instanceof UnknownHostException) {
// Timeout
return true;
}
HttpClientContext clientContext = HttpClientContext.adapt(context);
HttpRequest request = clientContext.getRequest();
boolean idempotent = !(request instanceof HttpEntityEnclosingRequest);
if (idempotent) {
// Retry if the request is considered idempotent
return true;
}
return false;
}
};
或者在统一的网络请求方法外套用封装,自定义要捕获的异常,进行重试
这个方案不优雅,废弃……
单元测试框架层面
Junit框架
junit4之后提供了新特性@Rule功能,我们可以使用这个能力来做失败重试
@Rule 注解是方法级别的,每个测试⽅法执⾏时都会执⾏,类似@Before
我们可以自定义实现一个RetryRule
public class RertyRule implements TestRule{
private int loopCount;
public RertyRule(int loopCount) {
this.loopCount = loopCount;
}
@Override
public Statement apply(final Statement base, Description description) {
return new Statement() {
//在测试方法执行的前后分别打印消息
@Override
public void evaluate() throws Throwable {
for (int i = 0; i < loopCount; i++) {
try {
//执行用例
base.evaluate();
log.info("-----------RetryRunner-----------: Test case success, " + (i + 1));
return;
} catch (Throwable t) {
caughtThrowable = t;
log.warn("-----------RetryRunner-----------: Test case failed, " + (i + 1) + ", " + getExceptionMsg(caughtThrowable));
}
if(i=loopCount){
log.error("-----------RetryRunner-----------: Test case finally failed, retryTimes max" );
}
}
}
};
}
}
在用例中使用是这样的
@Rule
public RetryRule retryRule = new RetryRule(3);
@Test
public void testMethodA() throws Exception
{
System.out.println("test methodA...");
}
当然这样写就只能把测试类下面所有的方法都做重试,如果想针对某一个测试方法单独重试,就不是很灵活,这样我们还可以追加自定义注解的方式来优化
先自定义一个方法注解,然后应用在方法级别
//自定义Retry注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Retry {
int times();
}
//改写RetryRule
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
public class RetryRule implements TestRule{
@Override
public Statement apply(Statement statement, Description description)
{
return new Statement() {
@Override
public void evaluate() throws Throwable
{
Throwable retryThrowable = null;
Retry retry = description.getAnnotation(Retry.class);
if(retry != null)
{
int times = retry.times();
for(int i=0; i<times; i++)
{
try
{
statement.evaluate();
return;
}
catch(final Throwable t)
{
retryThrowable = t;
System.err.println("Run method " + description.getMethodName() + ": failed for " + (i+1) + ((i+1) == 1 ? " time" : " times "));
}
}
System.err.println("Run method " + description.getMethodName() + " : exited after " + times + " attempts");
}
else
{
statement.evaluate();
}
}
};
}
}
//待测试类
import static org.junit.Assert.fail;
import org.junit.FixMethodOrder;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runners.MethodSorters;
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class MyRetryRuleTest
{
@Rule
public RetryRule retryRule = new RetryRule();
@Test
public void testMethodA() throws Exception
{
System.out.println("test methodA...");
}
@Test
@Retry(times=3)
public void testMethodB() throws Exception
{
fail();
System.out.println("test methodB...");
}
@Retry(times=5)
@Test(timeout=10)
public void testMethodC() throws Exception
{
Thread.sleep(10);
System.out.println("test methodC...");
}
}
TestNG框架
实现IRetryAnalyzer类的方式来用注解的方法来做方法级别的重试
public class TestNGRetry implements IRetryAnalyzer {
private int retryCount = 1;
private static final int maxRetryCount = 3;
@Override
public boolean retry(ITestResult result) {
if (retryCount<=maxRetryCount){
retryCount++;
return true;
}
return false;
}
public void reSetCount(){
retryCount=1;
}
}
在测试方法上面,这么加注解即可
@Test(retryAnalyzer= TestNGRetry.class)
当然这个是单个测试方法级别,如果想实现批量的配置,可以写个监听器,放到xml配置里面,或者注解在测试类上,这样所有的测试用例都能用这个重试方法了
public class RetryListener implements IAnnotationTransformer {
@Override
public void transform(ITestAnnotation annotation, Class testClass, Constructor testConstructor,
Method testMethod) {
IRetryAnalyzer retryAnalyzer = annotation.getRetryAnalyzer();//获取到retryAnalyzer的注解
if (retryAnalyzer == null){
annotation.setRetryAnalyzer(TestNGRetry.class);
}
}
}
配置文件中
<?xml version="1.0" encoding="UTF-8"?>
<suite name="Suite" parallel="false" thread-count="2">
<listeners>
<listener
class-name="chongshi.tesng.TestRunnerListener" />
<listener class-name="chongshi.tesng.RetryListener"/>
</listeners>
<test name="Test">
<classes>
<class name="chongshi.tesng.New"/>
</classes>
</test> <!-- Test -->
</suite> <!-- Suite -->
或者直接注解在类上
@Listeners({RetryListener.class})
public class TestNGReRunDemo {
@Test
public void test01(){
Assert.assertEquals("success","fail");
System.out.println("test01");
}
}
执行框架层面
由于我们的脚本框架使用了maven来做依赖管理,同时使用了maven-surefire-plugin插件,所以可以用插件中的重试能力
增加如下配置,可以做到某个用例失败后重试2次
<configuration>
<rerunFailingTestsCount>2</rerunFailingTestsCount>
</configuration>
最终经历过失败重试,最终成功的用例会在日志中这么展示
Results :
Flaked tests:
com.qyf404.learn.maven.CaseTest.test01(com.qyf404.learn.maven.CaseTest)
Run 1: CaseTest.test01:32 expected:<2> but was:<3>
Run 2: CaseTest.test01:32 expected:<2> but was:<3>
Run 3: PASS
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Flakes: 1
要注意该插件同时支持junit 和testNg,但是有版本限制,建议junit版本大于4.12
maven-surefire-plugin插件功能相当全面,建议参考这边文章 https://www.cnblogs.com/qyf404/p/5013694.html
总结:
方案 | 简述 | 优点 | 缺点 |
---|---|---|---|
网络请求层面 | 修改封装后的httpclient | 简单直接;颗粒度到了请求级别,比方法级别更细 | 只能全局使用;只能针对涉及到网络请求的重试,UI的脚本就不太行了;重试次数写死了 |
单测框架层面 | junit和testNg的注解或者Rule的自定义实现 | 使用灵活,全局/测试集/测试方法级别都可以使用,也可以自定义重试次数 | 相对来说需要了解框架原理,新人上手有难度 |
执行框架层面 | 引入maven-surefire-plugin插件 | 简单直接;还有很多其他很好用的功能 | 只能全局使用;重试次数写死了 |