Как перезапустить неудачные тесты JUnit немедленно?
Есть ли способ иметь правило JUnit или что-то подобное, что дает каждому провалу тест второй шанс, просто пытаясь запустить его еще раз.
Справочная информация: у меня есть большой набор тестов Selenium2-WebDriver, написанных с помощью JUnit. Из-за очень агрессивного времени (только короткие периоды ожидания после щелчков) некоторые тесты (1 из 100, и всегда другой) могут проваливаться, потому что сервер иногда реагирует немного медленнее. Но я не могу сделать период ожидания настолько длинным, чтобы он определенно был достаточно длинным, потому что тогда тесты будут длиться вечно.) - Поэтому я считаю приемлемым для этого варианта использования, что тест зеленый, даже если ему требуется секунда пытаться.
Конечно, было бы лучше иметь 2 из 3 большинства (повторить неудачный тест 3 раза и принять их как правильные, если два теста верны), но это будет улучшением в будущем.
7 ответов
Вы можете сделать это с помощью TestRule. Это даст вам необходимую гибкость. TestRule позволяет вам вставить логику вокруг теста, чтобы вы могли реализовать цикл повторных попыток:
public class RetryTest {
public class Retry implements TestRule {
private int retryCount;
public Retry(int retryCount) {
this.retryCount = retryCount;
}
public Statement apply(Statement base, Description description) {
return statement(base, description);
}
private Statement statement(final Statement base, final Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
Throwable caughtThrowable = null;
// implement retry logic here
for (int i = 0; i < retryCount; i++) {
try {
base.evaluate();
return;
} catch (Throwable t) {
caughtThrowable = t;
System.err.println(description.getDisplayName() + ": run " + (i+1) + " failed");
}
}
System.err.println(description.getDisplayName() + ": giving up after " + retryCount + " failures");
throw caughtThrowable;
}
};
}
}
@Rule
public Retry retry = new Retry(3);
@Test
public void test1() {
}
@Test
public void test2() {
Object o = null;
o.equals("foo");
}
}
Сердце TestRule
это base.evaluate()
, который вызывает ваш тестовый метод. Так что вокруг этого звонка вы поставили повторную петлю. Если в вашем методе тестирования возникает исключение (на самом деле ошибка подтверждения AssertionError
), тогда тест не пройден, и вы повторите попытку.
Есть еще одна вещь, которая может быть полезной. Возможно, вы захотите применить эту логику повторения только к набору тестов, и в этом случае вы можете добавить в класс Retry выше тест для конкретной аннотации метода. Description
содержит список аннотаций для метода. Для получения дополнительной информации об этом см. Мой ответ на Как запустить некоторый код перед каждым методом JUnit @Test индивидуально, без использования @RunWith или AOP?,
Использование собственного TestRunner
Это предложение CKuck, вы можете определить свой собственный бегун. Вам нужно расширить BlockJUnit4ClassRunner и переопределить runChild(). Для получения дополнительной информации см. Мой ответ на Как определить правило метода JUnit в комплекте?, В этом ответе подробно описано, как определить, как запускать код для каждого метода в наборе, для которого вы должны определить свой собственный Runner.
Как по мне, написание кастомного раннера более гибкое решение. Решение, опубликованное выше (с примером кода), имеет два недостатка:
- Он не будет повторять тестирование, если он потерпит неудачу на этапе @BeforeClass;
- В нем вычисления тестов выполняются немного иначе (когда у вас есть 3 повторные попытки, вы получите тестовые прогоны: 4, успех 1, который может сбить с толку);
Вот почему я предпочитаю больше подходить к написанию кастомного раннера. И код пользовательского бегуна может быть следующим:
import org.junit.Ignore;
import org.junit.internal.AssumptionViolatedException;
import org.junit.internal.runners.model.EachTestNotifier;
import org.junit.runner.Description;
import org.junit.runner.notification.RunNotifier;
import org.junit.runner.notification.StoppedByUserException;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;
public class RetryRunner extends BlockJUnit4ClassRunner {
private final int retryCount = 100;
private int failedAttempts = 0;
public RetryRunner(Class<?> klass) throws InitializationError {
super(klass);
}
@Override
public void run(final RunNotifier notifier) {
EachTestNotifier testNotifier = new EachTestNotifier(notifier,
getDescription());
Statement statement = classBlock(notifier);
try {
statement.evaluate();
} catch (AssumptionViolatedException e) {
testNotifier.fireTestIgnored();
} catch (StoppedByUserException e) {
throw e;
} catch (Throwable e) {
retry(testNotifier, statement, e);
}
}
@Override
protected void runChild(final FrameworkMethod method, RunNotifier notifier) {
Description description = describeChild(method);
if (method.getAnnotation(Ignore.class) != null) {
notifier.fireTestIgnored(description);
} else {
runTestUnit(methodBlock(method), description, notifier);
}
}
/**
* Runs a {@link Statement} that represents a leaf (aka atomic) test.
*/
protected final void runTestUnit(Statement statement, Description description,
RunNotifier notifier) {
EachTestNotifier eachNotifier = new EachTestNotifier(notifier, description);
eachNotifier.fireTestStarted();
try {
statement.evaluate();
} catch (AssumptionViolatedException e) {
eachNotifier.addFailedAssumption(e);
} catch (Throwable e) {
retry(eachNotifier, statement, e);
} finally {
eachNotifier.fireTestFinished();
}
}
public void retry(EachTestNotifier notifier, Statement statement, Throwable currentThrowable) {
Throwable caughtThrowable = currentThrowable;
while (retryCount > failedAttempts) {
try {
statement.evaluate();
return;
} catch (Throwable t) {
failedAttempts++;
caughtThrowable = t;
}
}
notifier.addFailure(caughtThrowable);
}
}
Теперь есть лучший вариант. Если вы используете плагины maven, такие как: surfire или failsefe, есть возможность добавить параметр rerunFailingTestsCount
SurFire Api. Этот материал был реализован в следующем билете: Jira Ticket. В этом случае вам не нужно писать свой собственный код, а плагин автоматически корректирует отчет с результатами теста.
Я вижу только один недостаток этого подхода: если какой-либо тест не пройден до / после теста на этапе класса, он не будет перезапущен.
Предлагаемый комментарий был написан на основе этой статьи с некоторыми дополнениями.
Здесь, если какой-то тестовый пример из вашего проекта jUnit получит результат "сбой" или "ошибка", этот тестовый случай будет перезапущен еще раз. Всего здесь мы поставили 3 шанса на успех.
Итак, нам нужно создать класс правил и добавить уведомления "@Rule" в ваш тестовый класс.
Если вы не хотите создавать одинаковые уведомления "@Rule" для каждого вашего тестового класса, вы можете добавить его в свой абстрактный класс SetProperty (если он у вас есть) и продолжить его.
Правило Класс:
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
public class RetryRule implements TestRule {
private int retryCount;
public RetryRule (int retryCount) {
this.retryCount = retryCount;
}
public Statement apply(Statement base, Description description) {
return statement(base, description);
}
private Statement statement(final Statement base, final Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
Throwable caughtThrowable = null;
// implement retry logic here
for (int i = 0; i < retryCount; i++) {
try {
base.evaluate();
return;
} catch (Throwable t) {
caughtThrowable = t;
// System.out.println(": run " + (i+1) + " failed");
System.err.println(description.getDisplayName() + ": run " + (i + 1) + " failed.");
}
}
System.err.println(description.getDisplayName() + ": giving up after " + retryCount + " failures.");
throw caughtThrowable;
}
};
}
}
Тестовый класс:
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
/**
* Created by ONUR BASKIRT on 27.03.2016.
*/
public class RetryRuleTest {
static WebDriver driver;
final private String URL = "http://www.swtestacademy.com";
@BeforeClass
public static void setupTest(){
driver = new FirefoxDriver();
}
//Add this notification to your Test Class
@Rule
public RetryRule retryRule = new RetryRule(3);
@Test
public void getURLExample() {
//Go to www.swtestacademy.com
driver.get(URL);
//Check title is correct
assertThat(driver.getTitle(), is("WRONG TITLE"));
}
}
Вы должны написать свой собственный org.junit.runner.Runner
и комментируйте свои тесты с @RunWith(YourRunner.class)
,
Что касается Junit5, есть классная функция @RetryingTest, предлагаемая расширением junit-pioneer https://junit-pioneer.org/docs/retrying-test/, простой пример:
public class RetryTest {
private int counter = 0;
@RetryingTest(5)
void retryCounter() {
if (counter++ < 2) fail();
}
}
Это дважды завершится ошибкой и будет отображаться зеленым цветом при третьем выполнении.
Этот ответ построен на этом ответе.
Если тебе нужен твой
ActivityScenario
(и ваше Activity), которое нужно воссоздавать перед каждым запуском, вы можете запустить его с помощью try-with-resources. В
ActivityScenario
будет автоматически закрываться после каждой попытки.
public final class RetryRule<A extends Activity> implements TestRule {
private final int retryCount;
private final Class<A> activityClazz;
private ActivityScenario<A> scenario;
/**
* @param retryCount the number of retries. retryCount = 1 means 1 (normal) try and then
* 1 retry, i.e. 2 tries overall
*/
public RetryRule(int retryCount, @NonNull Class<A> clazz) {
this.retryCount = retryCount;
this.activityClazz = clazz;
}
public Statement apply(Statement base, Description description) {
return statement(base, description);
}
private Statement statement(final Statement base, final Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
Throwable caughtThrowable = null;
// implement retry logic here
for (int i = 0; i <= retryCount; i++) {
try(ActivityScenario<A> scenario = ActivityScenario.launch(activityClazz)){
RetryRule.this.scenario = scenario;
base.evaluate();
return;
} catch (Throwable t) {
caughtThrowable = t;
Log.e(LOGTAG,
description.getDisplayName() + ": run " + (i + 1) + " failed: ", t);
}
}
Log.e(LOGTAG,
description.getDisplayName() + ": giving up after " + (retryCount + 1) +
" failures");
throw Objects.requireNonNull(caughtThrowable);
}
};
}
public ActivityScenario<A> getScenario() {
return scenario;
}
}
Затем вы можете получить доступ к своему сценарию в своих тестах, используя
getScenario()
метод.