TestNg 失败重跑(支持使用 dataProvider 的参数化用例)

最近在用 Java+TestNg+Maven 写 UI 自动化。因为之前用惯了 Python 的测试框架,失败重跑装个插件(flaky 或者 pytest-rerunfailures)就行。而 TestNg 的失败重跑需要自己重新方法,并且网上搜了很多资料,针对使用了dataProvider的参数化用例都存在一些问题。因此希望这篇文章能对需要的人起到帮助。

总体方案与网上能搜到大同小异:

  1. 新建一个继承IRetryAnalyzer接口的类,这个类主要用于写失败重跑的规则
  2. 新建一个继承IAnnotationTransformer接口的类,用于监听事件
  3. 在 TestNg 的 XML 文件中配置监听

那么就一步一步来。

新建Retry

首先,新建Retry类,继承IRetryAnalyzer接口,并自定义重跑规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.ntflc.listener;

import org.testng.IRetryAnalyzer;
import org.testng.ITestResult;

public class Retry implements IRetryAnalyzer {
private int retryCnt = 0;
private int maxRetryCnt = 2;

@Override
public boolean retry(ITestResult result) {
if (retryCnt < maxRetryCnt) {
retryCnt++;
return true;
}
return false;
}
}

其中maxRetryCnt是每个用例最多重试的次数(不包括第 1 次执行),retryCnt是已经重跑的次数。retry方法判断如果已经重跑的次数retryCnt小于设定的总次数,则返回true进行重跑,同时retryCnt加 1;否则返回false不再重跑。

下文均以最多重跑 2 次为例。

新建RetryListener

然后,新建RetryListener类,继承IAnnotationTransformer接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.ntflc.listener;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import org.testng.IAnnotationTransformer;
import org.testng.IRetryAnalyzer;
import org.testng.annotations.ITestAnnotation;

public class RetryListener implements IAnnotationTransformer {
public void transform(ITestAnnotation annotation, Class testClass, Constructor testConstructor, Method testMethod) {
IRetryAnalyzer retry = annotation.getRetryAnalyzer();
if (retry == null) {
annotation.setRetryAnalyzer(Retry.class);
}
}
}

配置监听

最后,在 TestNg 的 XML 文件中配置监听:

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >
<suite name="MyTest">
<listeners>
<listener class-name="com.ntflc.listener.RetryListener"/>
</listeners>
<test name="Test1">
...
</test>
</suite>

这样,对于非使用了dataProvider的用例,如果失败会进行重跑,最多跑 2 次。

问题与解决

存在的问题

上面的做法是网上绝大多数文章的全部内容,但对于一个使用了dataProvider的用例,因为这个用例是一个标记为@Test的方法,会共用RetryretryCnt,即整个方法的所有参数化用例,总共只会重跑 2 次。例如一个参数化用例有 3 组参数,如果全部正确,结果是:

1
2
3
Test1: success
Test2: success
Test3: success

如果第 1 个用例失败 1 次(第 2 次成功),第 2 个用例如果均失败,总共只跑了 2 次。因为第 1 个用例第 1 次失败时,retryCnt为 0 并进行重跑;第 2 个用例第 1 次失败后,retryCnt为 1 并进行重跑;第 2 个用例第 2 次失败后,retryCnt为 2 因此不再重跑。即:

1
2
3
4
5
6
Test1: failed -> skipped
Test1: suceees
Test2: failed -> skipped
Test2: failed
Test3: failed
Test3: failed

至于为什么Test3也重跑了 1 次,这里不太清楚,因为Test3第 1 次失败时,retryCnt为 2 返回的是false,不应该再进行重跑。这里不清楚是不是 TestNg 的 Bug。

对此,网上有部分文章,会在Retryreturn false;前设置retryCnt = 0;即:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.ntflc.listener;

import org.testng.IRetryAnalyzer;
import org.testng.ITestResult;

public class Retry implements IRetryAnalyzer {
private int retryCnt = 0;
private int maxRetryCnt = 2;

@Override
public boolean retry(ITestResult result) {
if (retryCnt < maxRetryCnt) {
retryCnt++;
return true;
}
retryCnt = 0;
return false;
}
}

但这样只有在retryCnt达到maxRetryCnt后才会重置。即对于每个参数化用例都失败的情况,这样是没问题的:

1
2
3
4
5
6
7
8
9
Test1: failed -> skipped
Test1: failed -> skipped
Test1: failed
Test2: failed -> skipped
Test2: failed -> skipped
Test2: failed
Test3: failed -> skipped
Test3: failed -> skipped
Test3: failed

但如果一旦有一个参数化用例没有跑到maxRetryCnt的次数,retryCnt就不会重置为 0,如:

1
2
3
4
5
Test1: failed -> skipped
Test1: success
Test2: failed -> skipped
Test2: failed
Test3: success

因为Test1失败了一次,重跑后retryCnt为 1。当Test2第 1 次失败时,此时retryCnt为 1(没有重置为 0),可以重跑,但返回trueretryCnt就变为 2 了,从而导致第 2 次失败时,判断为false不再重跑。因此Test2只重跑了 1 次就直接标为失败。

解决方法

解决上述问题的方法其实很简单,即每个参数化的用例结束(无论成功、失败)后,重置retryCnt

这里我们先在Retry中增加一个重置retryCnt的方法reset

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.ntflc.listener;

import org.testng.IRetryAnalyzer;
import org.testng.ITestResult;

public class Retry implements IRetryAnalyzer {
private int retryCnt = 0;
private int maxRetryCnt = 2;

@Override
public boolean retry(ITestResult result) {
if (retryCnt < maxRetryCnt) {
retryCnt++;
return true;
}
return false;
}

// 用于重置retryCnt
public void reset() {
retryCnt = 0;
}
}

然后新建TestngListener类,继承TestListenerAdapter类,并重写onTestSuccessonTestFailure方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.ntflc.listener;

import org.testng.TestListenerAdapter;
import org.testng.ITestResult;

public class TestngListener extends TestListenerAdapter {
@Override
public void onTestSuccess(ITestResult tr) {
super.onTestSuccess(tr);
// 对于dataProvider的用例,每次成功后,重置Retry次数
Retry retry = (Retry) tr.getMethod().getRetryAnalyzer();
retry.reset();
}

@Override
public void onTestFailure(ITestResult tr) {
super.onTestFailure(tr);
// 对于dataProvider的用例,每次失败后,重置Retry次数
Retry retry = (Retry) tr.getMethod().getRetryAnalyzer();
retry.reset();
}
}

最后,在 TestNg 的 XML 中配置该监听:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >
<suite name="MyTest">
<listeners>
<listener class-name="com.ntflc.listener.RetryListener"/>
<listener class-name="com.ntflc.listener.TestngListener"/>
</listeners>
<test name="Test1">
...
</test>
</suite>

这样,对于使用了dataProvider用例中的每一个参数化用例,都会最多跑 2 次,无论最后成功还是失败,都会重置Retry中的retryCnt以保证下一个参数化用例开始时,retryCnt为初始状态。

以上就是本文的全部内容,由于本人使用 TestNg 时间较短,Java 基础也比较薄弱,难免会有疏漏,欢迎交流。

如果你喜欢我的文章,欢迎打赏。