脚本稳定性方案:用例失败自动重试

背景

在我们执行自动化测试脚本的时候,可能会因为某个时刻的网络较差,或者某些客观原因导致用例失败,但是很多时候重跑一下脚本的时候就好了。为了提高脚本执行稳定性,也为了减少我们测试同学排查脚本报错的工作量,需要能让用例失败的时候能够自己自动的重试

需求分析

  • 要能够做到失败时自动重试,重试时保留测试参数不变
  • 希望是用例级别的重试,而不是整个用例集,因为那样太浪费时间
  • 希望可以对于脚本的侵入最小化,因为我们的用例数太多,每个都要改的话工作量太大,要能既支持单个用例,又能针对用例集配置

方案选型(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插件简单直接;还有很多其他很好用的功能只能全局使用;重试次数写死了
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值