Skip to content

JUnit 5: @RepeatedTest in Depth

The Elevator Button Stress Test

Imagine an engineering team designing the control panel for a skyscraper elevator. Before approving the push buttons for installation, an automated mechanical tester presses the floor button one thousand times in succession. The goal is not to test whether the elevator moves once — they already know it does. The goal is to detect intermittent mechanical jamming, signal delays that only emerge after prolonged operation, or electronic race conditions that appear once every hundred presses.

In software, certain bugs do not manifest on a single execution. Multithreaded race conditions, memory pool leaks, intermittent network timeouts, and non deterministic random algorithms can pass a unit test ninety nine times and fail on the hundredth. For these scenarios, JUnit 5 provides the @RepeatedTest annotation, allowing you to repeat a test method a specified number of times with full control over repetition metadata and reporting.

This lecture covers @RepeatedTest syntax, configuring custom repetition display names, injecting the RepetitionInfo parameter, and practical use cases for flaky test detection and idempotency verification.


Basic Syntax of @RepeatedTest

To repeat a test, replace standard @Test with @RepeatedTest and specify the total number of repetitions:

java
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.RepeatedTest;

import static org.junit.jupiter.api.Assertions.assertTrue;

class RepetitionBasicsTest {

    @RepeatedTest(5)
    @DisplayName("Verify secure token generation")
    void generateToken_shouldAlwaysBeValid() {
        TokenGenerator generator = new TokenGenerator();
        String token = generator.generate();

        assertTrue(token.startsWith("SEC-"), "Token must begin with prefix SEC-");
        assertTrue(token.length() >= 32, "Token must be at least 32 characters long");
    }
}

When you execute this test:

  • The method runs five times.
  • Each repetition counts as an independent test execution in your IDE test runner.
  • If repetition number three fails, repetitions four and five still execute, and JUnit highlights repetition three as the failed run.
  • In default per method lifecycle mode, JUnit creates a fresh test instance for each repetition, ensuring state isolation.

Customizing Repetition Display Names

By default, JUnit formats each repetition name as repetition 1 of 5, repetition 2 of 5, and so on. You can customize this display format using the name attribute of @RepeatedTest.

JUnit provides three predefined placeholders:

PlaceholderMeaning
{displayName}The display name of the method (from @DisplayName or method name)
{currentRepetition}The index of the current repetition (1 based)
{totalRepetitions}The total count of repetitions configured
java
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.RepeatedTest;

class CustomDisplayNameTest {

    @RepeatedTest(
        value = 3,
        name = "{displayName} -> Executing cycle {currentRepetition} out of {totalRepetitions}"
    )
    @DisplayName("ProcessOrderCycle")
    void processOrder() {
        // Runs 3 times with custom label in the test tree:
        // "ProcessOrderCycle -> Executing cycle 1 out of 3"
        // "ProcessOrderCycle -> Executing cycle 2 out of 3"
        // "ProcessOrderCycle -> Executing cycle 3 out of 3"
    }
}

JUnit also provides two predefined constants in RepeatedTest:

  • RepeatedTest.SHORT_DISPLAY_NAME: Displays repetition {currentRepetition} of {totalRepetitions}
  • RepeatedTest.LONG_DISPLAY_NAME: Displays {displayName} :: repetition {currentRepetition} of {totalRepetitions}

Injecting RepetitionInfo

Often your test logic needs to know which repetition is currently running. You might want to log the iteration number, vary a seed slightly, or assert different behavior on the final repetition.

JUnit 5 provides the RepetitionInfo parameter, which you can inject directly into your test method or into @BeforeEach and @AfterEach lifecycle methods:

java
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.RepetitionInfo;

import static org.junit.jupiter.api.Assertions.assertEquals;

class RepetitionInfoInjectionTest {

    @BeforeEach
    void setup(RepetitionInfo info) {
        System.out.println("Preparing for repetition " + info.getCurrentRepetition()
            + " of " + info.getTotalRepetitions());
    }

    @RepeatedTest(4)
    void testVaryingIteration(RepetitionInfo info) {
        int current = info.getCurrentRepetition();
        int total = info.getTotalRepetitions();

        System.out.println("Running repetition index: " + current);

        assertEquals(4, total);

        if (current == total) {
            System.out.println("Final iteration cleanup verification");
        }
    }
}

The RepetitionInfo interface provides two methods:

  1. int getCurrentRepetition(): Returns the one based index of the active repetition.
  2. int getTotalRepetitions(): Returns the total number of repetitions declared.

Real World Use Cases for @RepeatedTest

1. Detecting Intermittent and Flaky Bugs

A flaky test is one that passes most of the time but fails intermittently due to uncoordinated concurrency, race conditions, or timing assumptions. Running the test once during a build may miss the race condition. Repeating the test one hundred times in a dedicated stress test suite dramatically increases the probability of exposing concurrency defects:

java
@RepeatedTest(50)
void concurrentQueue_shouldNeverDropElements() throws InterruptedException {
    ConcurrentWorkerQueue queue = new ConcurrentWorkerQueue();
    runConcurrentProducersAndConsumers(queue);

    assertEquals(0, queue.getDroppedCount(), "Queue should never drop tasks under load");
}

2. Testing Idempotency

In API design, an idempotent operation is one that can be invoked multiple times with the same inputs without changing the result beyond the initial application. Payment confirmations, user account activation, and cache updates must be idempotent:

java
@RepeatedTest(3)
void activateUser_shouldBeIdempotent() {
    UserAccount account = userManager.getAccount("user-88");
    account.activate();

    assertEquals(AccountStatus.ACTIVE, account.getStatus());
    assertEquals(1, account.getActivationEventCount(), "Only one activation event should be recorded");
}

3. Benchmarking and Warm Up

When measuring execution times, the first run often incurs JVM class loading and just in time compilation overhead. You can repeat a test multiple times to observe steady state execution time:

java
@RepeatedTest(10)
void cryptographicHash_shouldCompleteWithinTargetTime(RepetitionInfo info) {
    long start = System.nanoTime();
    hasher.computeHash("sample-payload-data");
    long durationMs = (System.nanoTime() - start) / 1_000_000;

    // Disregard cold JVM execution on repetition 1
    if (info.getCurrentRepetition() > 1) {
        assertTrue(durationMs < 50, "Steady state execution should complete under 50ms");
    }
}

@RepeatedTest vs @ParameterizedTest

Developers often wonder whether to use repeated tests or parameterized tests. The distinction is straightforward:

Feature@RepeatedTest@ParameterizedTest
Input DataIdentical input for every executionDifferent input data per invocation
Parameter InjectionInjects RepetitionInfoInjects values from @ValueSource, @CsvSource, etc.
Primary GoalDetect intermittent bugs, test idempotency, verify stabilityVerify algorithm correctness across various input data domains
Annotation@RepeatedTest(n)@ParameterizedTest + data source annotation

If your goal is to test that your validation logic rejects five different invalid email formats, use @ParameterizedTest. If your goal is to execute the exact same payment authorization five times to verify that race conditions do not double charge, use @RepeatedTest.


Interview Questions & Pitfalls

Q1: Can @Test and @RepeatedTest be placed on the same method?

No. You should not place both @Test and @RepeatedTest on the same method. If you do, JUnit 5 treats them as two separate test declarations, executing the method once as a standard test and then repeating it for the configured count, resulting in duplicate runs. Use only @RepeatedTest.

Q2: What is the lifecycle behavior of @BeforeEach with @RepeatedTest?

@BeforeEach executes before every single repetition, and @AfterEach executes after every single repetition. In default per method mode, a fresh instance of the test class is instantiated for each repetition, ensuring complete isolation across repetitions.

Q3: How do you access the current repetition number inside a test method?

Declare a parameter of type RepetitionInfo in your test method signature: void testMethod(RepetitionInfo info). JUnit's Jupiter engine automatically resolves and injects this parameter, allowing you to call info.getCurrentRepetition() and info.getTotalRepetitions().

Q4: Can RepetitionInfo be injected into standard @Test methods?

No. RepetitionInfo can only be resolved inside methods annotated with @RepeatedTest, or in lifecycle methods (@BeforeEach, @AfterEach) associated with a repeated test. Attempting to inject it into a regular @Test method throws a ParameterResolutionException.

Q5: What placeholders are supported in the name attribute of @RepeatedTest?

JUnit 5 supports three placeholders: {displayName} (the method display name), {currentRepetition} (the index of the active repetition), and {totalRepetitions} (the total number of repetitions configured).