skip to content

In JUnit 5 (Jupiter), how do you make a single test method execute several times in a row, and how do you keep each execution distinguishable in the test report?

level: juniorimportance: should knowfreq 40%

answer

  1. @RepeatedTest(n) replaces @Test
  2. @TestTemplate → container + N child invocations
  3. @BeforeEach/@AfterEach per repetition, new instance too
  4. Default name: "repetition {currentRepetition} of {totalRepetitions}"
  5. SHORT_DISPLAY_NAME vs LONG_DISPLAY_NAME

basics

~20 s

Replace @Test with @RepeatedTest(n). Jupiter runs the method n times as n separate tests, each with its own @BeforeEach/@AfterEach. Default names read "repetition 1 of 10"; customise them with the annotation's name attribute using {currentRepetition} and {totalRepetitions}.

solid answer

~50 s

You swap `@Test` for `@RepeatedTest(10)`. It is a `@TestTemplate`, so the method becomes a **container** whose children are ten individual invocations — each one is reported separately, and the full per-test lifecycle (`@BeforeEach`, `@AfterEach`, and with the default per-method instance lifecycle a **fresh test-class instance**) runs for every repetition. A failure in repetition 3 does not stop repetitions 4-10 unless you set `failureThreshold`. The default display name is `"repetition {currentRepetition} of {totalRepetitions}"` (the constant `RepeatedTest.SHORT_DISPLAY_NAME`). You override it with the `name` attribute and the placeholders `{displayName}`, `{currentRepetition}`, `{totalRepetitions}` — or use the built-in `RepeatedTest.LONG_DISPLAY_NAME`, which prefixes the method's own display name. ```java @RepeatedTest(value = 5, name = "{displayName} — run {currentRepetition}/{totalRepetitions}") @DisplayName("token generator produces unique ids") void generatesUniqueIds() { ... } ``` Most suites need it rarely: for randomised input, warm-up-sensitive code, or to demonstrate that a flake fix holds.

code

java · 19 lines
java
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.RepeatedTest;
import static org.junit.jupiter.api.Assertions.assertTrue;

class TokenGeneratorTest {

    private final TokenGenerator generator = new TokenGenerator();

    @DisplayName("token generator produces non-blank ids")
    @RepeatedTest(value = 5, name = "{displayName} - run {currentRepetition}/{totalRepetitions}")
    void producesNonBlankIds() {
        assertTrue(generator.next().length() > 0);
    }

    @RepeatedTest(value = 3, name = RepeatedTest.LONG_DISPLAY_NAME)
    void usesTheBuiltInLongTemplate() {
        assertTrue(generator.next().startsWith("tok_"));
    }
}

go deeper

for a junior

Know that @RepeatedTest(n) replaces @Test, that each run is reported separately, and that the default name is "repetition X of Y".

for a middle

Add the lifecycle detail — per-repetition @BeforeEach/@AfterEach and a fresh instance — and the name template placeholders, and contrast it with a hand-written loop.

for a senior

Talk about what does and does not reset between repetitions (instance state resets, static/DB/singleton state does not) and the cost of multiplying an expensive setup by the repetition count.

for a principal

Frame when repetition belongs in the permanent suite at all versus being a temporary diagnostic, and the reporting/CI-time consequences of N-fold test node explosion.

## What `@RepeatedTest` is `@RepeatedTest` is a JUnit Jupiter annotation that replaces `@Test` on a method and tells the engine to execute that method a fixed number of times. `@RepeatedTest(10)` runs the method ten times. Mechanically it is not a special case of `@Test`: `@RepeatedTest` is meta-annotated with `@TestTemplate`. A test template is a method that produces *many* invocations, each of which the engine reports as its own test. That is why, in an IDE or an XML report, a repeated test appears as a **container node** (named after the method) with N **child nodes** underneath it, rather than as one test that happened to loop internally. The distinction matters. If you wrote the loop yourself — ```java @Test void generatesUniqueIds() { for (int i = 0; i < 10; i++) { assertNotNull(generator.next()); } } ``` — you would get **one** test result, the first failure would abort the remaining iterations, and setup would run once for all ten iterations. `@RepeatedTest` gives you ten results, ten independent setups, and (by default) execution of every repetition regardless of earlier failures. ## Lifecycle per repetition Each repetition is a full test execution: - `@BeforeEach` and `@AfterEach` run **once per repetition**. - With the default `Lifecycle.PER_METHOD` test-instance lifecycle, a **new instance of the test class** is constructed for each repetition, so instance fields are reset every time. - `@BeforeAll` / `@AfterAll` still run once for the whole class, not per repetition. - Extensions that hook per-test callbacks fire per repetition. The practical consequence: instance state does not leak between repetitions, but anything *outside* the instance does — static fields, singletons, a shared database, an embedded server, files on disk. If a repeated test passes on repetition 1 and fails on repetition 2, the usual culprit is exactly that kind of shared state (for example, an insert that violates a unique constraint the second time round). ## Display names and the name template Because each repetition is its own reported test, it needs its own name. The annotation's `name` attribute is a template with three placeholders: - `{displayName}` — the display name of the method itself (from `@DisplayName`, or the generated name). - `{currentRepetition}` — the 1-based index of this repetition. - `{totalRepetitions}` — the configured total. Two constants ship with the annotation: - `RepeatedTest.SHORT_DISPLAY_NAME` = `"repetition {currentRepetition} of {totalRepetitions}"` — this is the **default**. - `RepeatedTest.LONG_DISPLAY_NAME` = `"{displayName} :: repetition {currentRepetition} of {totalRepetitions}"`. So out of the box you see `repetition 3 of 10`, and the enclosing container carries the method's display name. Setting `name = RepeatedTest.LONG_DISPLAY_NAME` repeats the method name on every child line, which is useful when your CI report flattens the tree and the container name is lost. The `value` attribute (the repetition count) must be positive; `@RepeatedTest(0)` or a negative count is a configuration error, not a silently skipped test. ## When it earns its place Legitimate uses: - **Randomised or property-ish input** — the method draws a fresh random value each run and you want many samples reported individually. - **Warm-up-sensitive behaviour** — caches, lazy initialisation, connection pools that behave differently on the first call than on the hundredth. - **Proving a fix** — you temporarily crank the count while you demonstrate that a previously intermittent test is now stable, then dial it back. - **Idempotency checks** — running the same operation repeatedly must converge to the same state. It is a weak permanent tool for hunting flaky tests, because every repetition runs in the same JVM, on the same thread, in the same order — which is precisely the configuration that just passed. ## Common mistakes - Leaving `@Test` *and* `@RepeatedTest` on the same method. `@Test` is not a repeatable/composable partner here; the method should carry only `@RepeatedTest`. - Assuming setup runs once. It runs per repetition, so an expensive `@BeforeEach` multiplied by 200 repetitions is a slow suite. - Assuming a failure stops the rest. By default it does not — every repetition still runs (see `failureThreshold` if you want early exit). - Depending on order: repetition 5 must not rely on state left by repetition 4. If it does, you have written a stateful sequence, not a repeated test.

  • If repetition 2 of a @RepeatedTest fails, what happens to repetitions 3 through 10?
    By default they all still run. Each repetition is an independent test invocation, so a failure is recorded against that repetition only and execution continues. That is deliberate — you usually want to know whether it fails 1 time in 10 or 9 times in 10. If you want the engine to stop early, set the annotation's failureThreshold attribute, which skips the remaining repetitions once the given number of failures has been recorded.
  • How is @RepeatedTest(10) different from writing a for-loop with ten assertions inside a single @Test?
    The loop produces one test result, one setup/teardown pair, and stops at the first failed assertion, so you learn nothing about iterations after the failure. @RepeatedTest produces ten independently reported results, runs @BeforeEach/@AfterEach — and constructs a fresh test-class instance — for each one, and keeps going after a failure. That makes both the failure rate and the isolation between iterations visible.

A hand-rolled for-loop inside one @Test is a single sprint you either finish or don't; @RepeatedTest is ten separate heats, each with its own warm-up and its own line on the scoreboard.

saying these in an interview costs you the question

  • Saying @BeforeEach runs only once for the whole repeated test.
  • Believing the first failing repetition aborts the rest by default.
  • Putting both @Test and @RepeatedTest on the same method.
  • Assuming repetitions share the test-class instance, so instance fields accumulate across runs (they do not, under the default per-method lifecycle).
  • Thinking the ten repetitions are reported as a single test result rather than ten.

context