Wie führen Sie fehlgeschlagene JUnit-Tests sofort erneut aus?

Gibt es eine Möglichkeit, eine JUnit-Regel oder etwas Ähnliches zu haben, das jedem fehlgeschlagenen Test eine zweite Chance gibt, indem man versucht, es noch einmal auszuführen?

Hintergrund: Ich habe eine große Auswahl an seleniumium2-WebDriver-Tests, die mit JUnit geschrieben wurden. Aufgrund eines sehr aggressiven Timings (nur kurze Wartezeiten nach den Klicks) können einige Tests (1 von 100, und immer ein anderer) fehlschlagen, da der Server manchmal etwas langsamer reactjs. Aber ich kann die Wartezeit nicht so lang machen, dass es definitiv lang genug ist, denn dann werden die Tests ewig dauern.) – Also ich denke, es ist akzeptabel für diesen Anwendungsfall, dass ein Test grün ist, auch wenn er eine Sekunde braucht Versuchen.

Natürlich wäre es besser, eine 2 von 3 Mehrheit zu haben (wiederhole einen fehlgeschlagenen Test 3 Mal, und nimm sie als richtig, wenn zwei der Tests richtig sind), aber dies wäre eine zukünftige Verbesserung.

   

Sie können dies mit einer TestRule tun . Dies gibt Ihnen die Flexibilität, die Sie brauchen. Mit TestRule können Sie Logik um den Test herum einfügen, sodass Sie die Wiederholungsschleife implementieren würden:

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"); } } 

Das Herz einer TestRule ist die base.evaluate() , die Ihre Testmethode aufruft. Um diesen Aufruf herum setzen Sie eine Wiederholungsschleife. Wenn eine Ausnahme in Ihrer Testmethode ausgetriggers wird (ein Assertionserrors ist tatsächlich ein AssertionError ), ist der Test fehlgeschlagen und Sie versuchen es erneut.

Es gibt noch eine andere Sache, die nützlich sein kann. Sie können diese Wiederholungslogik nur auf eine Reihe von Tests anwenden. In diesem Fall können Sie in die Retry-class oberhalb eines Tests für eine bestimmte Annotation in der Methode einfügen. Description enthält eine Liste mit Anmerkungen für die Methode. Weitere Informationen hierzu finden Sie in meiner Antwort zu Wie kann ich vor jeder JUnit @ Test-Methode einen Code ausführen, ohne @RunWith oder AOP zu verwenden? .

Verwenden eines benutzerdefinierten TestRunner

Dies ist der Vorschlag von CKuck, Sie können Ihren eigenen Runner definieren. Sie müssen BlockJUnit4ClassRunner erweitern und runChild () überschreiben. Weitere Informationen finden Sie in meiner Antwort zur Definition der JUnit-Methodenregel in einer Suite. . In dieser Antwort wird beschrieben, wie Sie festlegen, wie Code für jede Methode in einer Suite ausgeführt wird, für die Sie einen eigenen Runner definieren müssen.

Wie für mich schreiben benutzerdefinierte Läufer flexiblere Lösung. Die obige Lösung (mit Codebeispiel) hat zwei Nachteile:

  1. Der Test wird nicht wiederholt, wenn er auf der Stufe @BeforeClass fehlschlägt.
  2. Die Berechnungstests laufen etwas anders ab (wenn Sie 3 Wiederholungen haben, erhalten Sie Testläufe: 4, Erfolg 1, der verwirrend sein könnte);

Deshalb bevorzuge ich mehr Ansatz mit dem Schreiben des kundenspezifischen Läufers. Und Code von benutzerdefinierten Läufer könnte folgen:

 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); } } 

Jetzt gibt es eine bessere Option. Wenn Sie Maven-Plugins wie: surfire oder failsefe verwenden, gibt es eine Option, Parameter rerunFailingTestsCount SurFire Api hinzuzufügen. Dieses Zeug wurde in das folgende Ticket implementiert: Jira Ticket . In diesem Fall müssen Sie Ihren benutzerdefinierten Code und das Plugin nicht automatisch ändern, um den Testergebnisbericht zu ändern.
Ich sehe nur einen Nachteil dieses Ansatzes: Wenn ein Test fehlgeschlagen ist, wird Vor / Nach dem classnstufentest nicht erneut ausgeführt.

Sie müssen Ihren eigenen org.junit.runner.Runner schreiben und Ihre Tests mit @RunWith(YourRunner.class) kommentieren.