junit5 icon indicating copy to clipboard operation
junit5 copied to clipboard

Introduce support for retrying failed flaky tests

Open arcuri82 opened this issue 5 years ago • 73 comments

Overview

Feature request to add native support to rerun failing flaky tests

Feature request

Is there any plan to support the ability of rerunning failed flaky tests? For example, it would be great to have something like @Flaky(rerun=3), which would rerun a failing flaky test up to n times.

In the past, in Surefire/Failsafe we could use rerunFailingTestsCount. However, it does not work in JUnit 5. As JUnit 5.0.0 was released nearly 1 year ago, it is unclear if and when it will be supported in Maven.

And, even if it will be supported in Maven in the future, it could be good to have a special annotation to specify that only some tests are expected to be flaky, and not all of them.

At the moment, the workaround is to stick with JUnit 4, which is a kind of a shame as JUnit 5 has a lot of interesting features :( (which I can only use in projects with no flaky tests)

Related SO question.

arcuri82 avatar Aug 20 '18 20:08 arcuri82

No, to my knowledge there are not currently any plans to implement such a feature.

But... I think it's a very good idea.

I've often wanted something like Spring Retry's @Retryable support for test methods. Of course, Spring Retry supports a much larger feature set than we would ever need/want in JUnit Jupiter.

Thanks for providing the link to the discussion on Stack Overflow. There are two interesting implementations there that can serve as inspiration.

Ideally, I'd like to see something like a @RetryableTest annotation that allows one to specify:

  • maximum number of retries
  • configurable exceptions that initiate a retry, defaulting to {Throwable.class}
  • potentially a minimum success rate

@junit-team/junit-lambda, thoughts?

sbrannen avatar Aug 21 '18 10:08 sbrannen

Tentatively slated for 5.4 M1 for team discussion

sbrannen avatar Aug 21 '18 10:08 sbrannen

configurable exceptions that initiate a retry, defaulting to {Throwable.class}

Hmm, how about defaulting to Exception.class or Throwable.class-except-blacklisted-exceptions?

My thinking is that in the off-chance an OutOfMemoryError is thrown, it should really be propagated to allow the JVM to shutdown as cleanly as it possibly can. :thinking:

jbduncan avatar Aug 21 '18 12:08 jbduncan

I don't like flaky tests (read: checks). Who does? I don't like the idea of "fixing" flaky tests by (naiv, smart, conditional, [what|for]-ever) re-execution. I don't want to support that in Jupiter.

Easy work-around:

@Test void test() { REPEAT flaky() UNTIL flaky-signals-green OR break-out }
boolean flaky() { ... return true : false ... }

Perhaps (!) such an annotation could live in an extension providing library: like https://junit-pioneer.org or another one. If we need to expose a new extension point tailored to make the "retry" functionality possible, well, I'm fine with that.

sormuras avatar Aug 21 '18 12:08 sormuras

@sormuras I do not like flaky tests either :) but unfortunately they are a reality :(

not an issue with unit tests (unless you work with randomized algorithms), but they are a big problem for E2E tests. There, it is hard to escape from flakyness. Even if a test wrongly fails 1 out 1000 times, when you have hundreds/thousands of E2E tests, there is a high probability that at least 1 fails, failing the whole build.

Of course one can use a try/catch in a loop, but that is a lot of boilerplate that has to be repeated each time for each test.

In the past, it was not an issue with JUnit 4, as this was externally handled with Maven. But this is not the case any more. In my case, this is a major showstopper for using JUnit 5. In some projects we tried JUnit 5 and were forced to revert to 4. In others, we use JUnit 5 for unit tests, but then have a (Maven) module for the E2E using JUnit 4.

arcuri82 avatar Aug 21 '18 13:08 arcuri82

Coincidentally, I recently hacked an extension that repeats failed tests:

class RepeatFailedTestTests {

	private static int FAILS_ONLY_ON_FIRST_INVOCATION;

	@RepeatFailedTest(3)
	void failsNever_executedOnce_passes() { }

	@RepeatFailedTest(3)
	void failsOnlyOnFirstInvocation_executedTwice_passes() {
		FAILS_ONLY_ON_FIRST_INVOCATION++;
		if (FAILS_ONLY_ON_FIRST_INVOCATION == 1) {
			throw new IllegalArgumentException();
		}
	}

	@RepeatFailedTest(3)
	void failsAlways_executedThreeTimes_fails() {
		throw new IllegalArgumentException();
	}

}

If JUnit 5 decides that flaky tests have no place in a proper testing framework, we pioneers don't have such scruples. :wink:

nipafx avatar Aug 21 '18 14:08 nipafx

Nice, @nicolaiparlog! 👍

I'm guessing you didn't know about the Rerunner Jupiter extension? 🤔

sbrannen avatar Aug 21 '18 14:08 sbrannen

Nope. :laughing: Most extensions I write come from excursions into the extension model, not from actual use cases (hence the lack of research).

Looks like @arcuri82 has at least one, maybe soon two options to achieve his goal of rerunning flaky tests in JUnit 5 - no need for native support. :wink:

nipafx avatar Aug 21 '18 14:08 nipafx

Related to "flaky tests": https://twitter.com/michaelbolton/status/1032125004480237568

sormuras avatar Aug 22 '18 14:08 sormuras

He should sooooo change his name - https://www.youtube.com/watch?v=ADgS_vMGgzY

smoyer64 avatar Aug 22 '18 17:08 smoyer64

According to @anonygoose in https://github.com/junit-team/junit5/issues/1079#issuecomment-418351588 the Rerunner extension is "dangerously buggy, and inactive".

@anonygoose Could you please post an example that showcases its bugginess?

marcphilipp avatar Sep 04 '18 18:09 marcphilipp

Hey Marc,

import io.github.artsok.RepeatedIfExceptionsTest;

public class RerunnerBugTest {

    @RepeatedIfExceptionsTest(repeats = 3)
    void test1() {
        System.out.println("Running test 1");
    }

    @RepeatedIfExceptionsTest(repeats = 3)
    void test2() throws Exception {
        System.out.println("Running test 2");
        throw new Exception("How does this not error?  It never runs!");
    }

    @RepeatedIfExceptionsTest(repeats = 3)
    void test3() {
        System.out.println("Running test 3");
    }

}

The above set of tests will run test1, and then skip test2 and test3. As soon as one test annotated with the annotation passes, it will skip any other tests annotated with it.

Uploaded the full example here: https://github.com/anonygoose/rerunner-bug-example

anonygoose avatar Sep 04 '18 19:09 anonygoose

Hi guys :)

I can make Pull Request with ReRun flasky mechanism, but i need information :)

artsok avatar Sep 04 '18 21:09 artsok

Getting official support for a retry feature would be great.

Ideally, I'd like to see something like a @RetryableTest annotation that allows one to specify:

  • maximum number of retries
  • configurable exceptions that initiate a retry, defaulting to {Throwable.class}
  • potentially a minimum success rate

Each of these points are configurable on the current Rerunner extension, and are useful. Carrying them over to any official support seems sensible.

Further consideration may need giving to:

  • Making retries work across the entire life-cycle, wherever the exception occurs (BeforeEach, AfterEach, etc)
  • What the proper outcome of the test is. Current rerun implementations end up with a lot of tests being reported as skipped. This is a bit messy when trying to interpret results.

anonygoose avatar Sep 05 '18 08:09 anonygoose

It would be nice to know whether the JUnit 5 team is considering implementing this.

  • If so, I will not continue working on my experiments.
  • If not, this issue can be closed and @artsok and I can decide how to move forward with his extension and my snippets.

@artsok: Would you consider integrating your extension into JUnit Pioneer? You would get more visibility and more hands to fix issues or implement features.

nipafx avatar Sep 06 '18 09:09 nipafx

...now on the list for tomorrow's team call.

sormuras avatar Sep 06 '18 09:09 sormuras

dangerously buggy, and inactive

I wanted to second this. I came to a similar conclusion a few weeks ago. I had to give up on other attempts, not feeling they were entirely safe, clean, light-weight, or stable. Instead, I ended up making my own copy of RepeatedTestExtension then made it use a custom TestTemplateInvocationContext Iterator with a TestExecutionExceptionHandler that conditionally throws a TestAbortedException. This seems to be the general approach I've seen floating around but at least I have my own light-weight solution that I have control over. I wish artsok and pioneer the best of luck but it would definitely be nice to have a clean, official, stable, and trusted solution.

benken-parasoft avatar Sep 06 '18 17:09 benken-parasoft

Just to add my 2 cents to this issue. We also have such flaky tests and were using the Surefire rerunFailingTestsCount in order to circumvent this. However, since our migration to JUnit Jupiter we noticed that this does not work anymore 😄. It would be great if there is something official from JUnit or maybe the appropriate plugins (Maven, Gradle) that would still have this support.

If you ask me I think that it is a bit better if we can do this expressively in the test itself and not have it global for the entire build.

filiphr avatar Sep 07 '18 06:09 filiphr

Team Decision: As a first step, we think this should be maintained externally as an extension based on @TestTemplate. We might potentially introduce support for rerunning flaky tests in core, but then it should support all kinds of testable methods, including @ParameterizedTests. We'd be happy to provide guidance and review such an extension, whether in the rerunner project, junit-pioneer or another project.

marcphilipp avatar Sep 07 '18 10:09 marcphilipp

I've just pushed my proof of concept to Pioneer, I'd like to implement that feature there and am interested in ideas, feedback, and even code if someone wants to write it. :smile_cat:

@marcphilipp This can be closed, right?

nipafx avatar Sep 07 '18 12:09 nipafx

Surefire can do this, but unfortunately only for JUnit 4: https://maven.apache.org/surefire/maven-surefire-plugin/examples/rerun-failing-tests.html I am not sure what the equivalent for Gradle is and if one of its plugins support retries.

What we need is that in the Surefire test result XML files the information about retries are contained. We use Allure for reporting, and I believe it processes these information in order to display how often a test has been repeated and what failures occured in each retry. Hopefully Surefile will soon implement this for JUnit 5, but I did not find any issue about that. I just created an issue for it.

A solution just for within an IDE will not help us at all. Think on CI/CD and the test result reporting.

I dislike the name ¨RepeatIfFailedTest¨, four words in a long camel case. Why not just @Retry?

tictac-freshmint avatar Oct 10 '18 12:10 tictac-freshmint

My team and I are very anxious to upgrade to JUnit 5, but this has been the hang up for us. May I ask if there are any updates for this? I have noticed that the Rerunner project and Pioneer have yet to implement the ability of stacking a Retry annotation on top of @ParameterizedTests which is key to what we are wanting to accomplish.

ekmaughan avatar Apr 24 '19 05:04 ekmaughan

@ekmaughan Thanks for letting us know that this feature is important for you. I'm afraid I don't have any updates for you at this point. I agree that this would be a useful feature for integration tests that depend on external resources.

@arcuri82 Have you submitted an issue to the Surefire project for this?

marcphilipp avatar Apr 29 '19 14:04 marcphilipp

@marcphilipp Someone else already did: https://issues.apache.org/jira/browse/SUREFIRE-1584

arcuri82 avatar Apr 29 '19 14:04 arcuri82

Hey @marcphilipp

One more vote but we would prefer configuration like the surefire rerunFailingTestsCount feature.

We have been using JUnit5 since its experimental days with great success. We are using it for integration testing of our API and like others, between network issues and platform issues, there is some level of unpredictability which is magnified because we are hitting our api so hard now that we can run things in parallel.

We have many tests and no one test is likely to be more "flaky" than another. The surefire rerunFailingTestsCount was actually the ideal solution for us rather than a special annotation per test. Ideally we would like to configure this kind of a feature for all tests.

I can see the use case for both a test specific annotation, or a configuration for all tests.

dlanaghen avatar Apr 30 '19 15:04 dlanaghen

@dlanaghen Makes sense! We could add a global config parameter for the default.

marcphilipp avatar Apr 30 '19 16:04 marcphilipp

Hey @ekmaughan

You can try to use my implementation with support @ParameterizedTests which I did today. As all main parameterized classes have default visibility I need to set mine owns. For that reason, I introduce a new annotation @ParameterizedRepeatedIfExceptionsTest.

Example:

    private ThreadLocalRandom random = ThreadLocalRandom.current();

    /**
     * By default total repeats = 1 and minimum success = 1.
     * If the test failed by this way start to repeat it by one time with one minimum success.
     *
     * This example with display name, repeated display name, 10 repeats and 2 minimum success with exceptions.
     * Exception depends on random number generation.
     */
    @DisplayName("User payment")
    @ParameterizedRepeatedIfExceptionsTest(name = "payment amount was {0}",
            repeatedName = " (Repeat {currentRepetition} of {totalRepetitions})",
            repeats = 10, exceptions = RuntimeException.class, minSuccess = 2)
    @ValueSource(ints = {40, 55, 61, 700})
    void errorParameterizedTestWithDisplayNameAndRepeatedName(int argument) {
        if (random.nextInt() % 2 == 0) {
            throw new RuntimeException("Exception in Test " + argument);
        }
    }

IDEA Report: изображение Explaining: First test with argument '40' failed. Repeat this test with first argument. Wait the sitaution when will have 2 minimum success runs. Then go to the next test with second argument and run it. With second argument way we can see that it was no 2 minimum success follow each other. Try to repeat 10 times but wihout result, go next test with argument '61' and so on.

Allure Report (another test run): изображение

Checked with this Junit version

<junit.jupiter.version>5.4.2</junit.jupiter.version>
<junit.platform.version>1.4.2</junit.platform.version>

You can try this extension:

<dependency>
    <groupId>io.github.artsok</groupId>
    <artifactId>rerunner-jupiter</artifactId>
    <version>2.0.1</version>
    <scope>test</scope>
</dependency>

The main logic you can find in ParameterizedRepeatedTestExtension.class. Source code you can find here. Don't forget to send star if u want.

artsok avatar May 04 '19 21:05 artsok

Thanks @artsok. Looking forward to when you’ve made sure it works parallelized too!

cypris avatar May 18 '19 19:05 cypris

Hi, Any progress on this ? We rely on the 'rerunFailingTestsCount' feature of maven-surefire-plugin and I just learned the hard way that it does not work with JUnit5.

toby1984 avatar Aug 12 '19 06:08 toby1984

@toby1984 While we'd still like to implement this, we haven't had time to look into it more closely, yet.

One open question is if/how we should report repetitions with "allowed failures". @artsok seems to have reported them as failed, I could also envision reporting them as aborted. Or we could not report them at all and only fail the test in the end if it didn't pass after the configured number of retries.

Another open question is the lifecycle of a repetition. Spock allows to configure which parts of the setup get executed for a retry. I think we can make it simple to start with and repeat the whole lifecycle like we do for repeated and parameterized tests.

Thoughts?

marcphilipp avatar Aug 24 '19 18:08 marcphilipp