A JUnit 5 test method annotated with @RepeatedTest needs to know which iteration it is currently on — for example to seed data differently on the first run. How do you get that information, and where else can it be injected?
answer
- Declare a RepetitionInfo parameter — built-in resolver
- getCurrentRepetition() is 1-based
- getTotalRepetitions(), getFailureThreshold() (5.10+)
- Injectable into @BeforeEach/@AfterEach — not @BeforeAll
- Mixing @Test in the same class → ParameterResolutionException
basics
~20 sDeclare a RepetitionInfo parameter on the method; JUnit's built-in resolver injects it. It exposes getCurrentRepetition(), getTotalRepetitions() and getFailureThreshold(). It can also be injected into @BeforeEach/@AfterEach — but only when that callback is running for a repeated test.
solid answer
~50 sAdd a `RepetitionInfo` parameter to the method signature. Jupiter has a built-in `ParameterResolver` for it, so nothing else is needed: ```java @RepeatedTest(3) void seedsPerRepetition(RepetitionInfo info) { if (info.getCurrentRepetition() == 1) { warmUpCache(); } } ``` `RepetitionInfo` gives you `getCurrentRepetition()` (1-based), `getTotalRepetitions()` and — since JUnit 5.10 — `getFailureThreshold()`. It can also be injected into `@BeforeEach` / `@AfterEach`, which is the usual place to prepare per-repetition fixtures. The catch: those callbacks are shared with ordinary `@Test` methods, and there is no repetition context for a plain `@Test`, so JUnit throws `ParameterResolutionException` for that class. Either put repeated tests in their own `@Nested` class with their own callbacks, or resolve the info inside the test method instead. Use it sparingly — branching on the repetition index means the repetitions are no longer identical runs, which is usually a smell.
code
java · 25 linesimport org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.RepetitionInfo;
import static org.junit.jupiter.api.Assertions.assertNotNull;
class AccountProvisioningTest {
private String username;
@BeforeEach
void setUp(RepetitionInfo repetitionInfo) {
// unique per repetition, so the DB unique constraint is never hit twice
username = "user-" + repetitionInfo.getCurrentRepetition();
}
@RepeatedTest(3)
void createsAccount(RepetitionInfo repetitionInfo) {
if (repetitionInfo.getCurrentRepetition() == 1) {
warmUpConnectionPool();
}
assertNotNull(provisioner.create(username));
}
private void warmUpConnectionPool() { /* ... */ }
}go deeper
Know that you add a RepetitionInfo parameter and read getCurrentRepetition() / getTotalRepetitions(), and that counting starts at 1.
Also know it is injectable into @BeforeEach/@AfterEach and that mixing plain @Test methods into that class causes ParameterResolutionException, plus the @Nested workaround.
Explain that it is plain parameter resolution via a built-in resolver, mention getFailureThreshold(), and call out branching on the index as a signal that the test wanted parameterisation instead.
Discuss where per-repetition variation belongs at all — deriving unique fixture keys to avoid shared-state collisions is fine; encoding distinct scenarios by index degrades reporting and should be refactored.
## The mechanism: parameter resolution JUnit Jupiter test methods may declare parameters, but only if some registered `ParameterResolver` can supply them. Jupiter ships built-in resolvers for `TestInfo`, `TestReporter` and `RepetitionInfo` (plus `@TempDir` via an extension). So the entire mechanism for "which repetition am I on" is: declare a parameter of type `RepetitionInfo` and the engine fills it in. ```java @RepeatedTest(5) void retriesConvergeToTheSameState(RepetitionInfo repetitionInfo) { int n = repetitionInfo.getCurrentRepetition(); ... } ``` No registration, no annotation on the parameter, no extension declaration. ## What `RepetitionInfo` exposes - `getCurrentRepetition()` — the **1-based** index of the repetition currently executing. The first repetition is `1`, not `0`. Off-by-one assumptions here are a classic bug. - `getTotalRepetitions()` — the value configured in `@RepeatedTest(value = ...)`. - `getFailureThreshold()` — added in JUnit 5.10; returns the configured `failureThreshold`, or `Integer.MAX_VALUE` when none was set (the default, meaning "never stop early"). That is the whole surface. It carries no history: it will not tell you how many repetitions have failed so far, or what happened in previous repetitions. If you need cross-repetition accumulation you have to hold it yourself in a `static` field or an extension store — and then you own the isolation problem that creates. ## Where it can be injected Beyond the test method itself, `RepetitionInfo` can be injected into `@BeforeEach` and `@AfterEach` callbacks. This is genuinely useful: it lets setup vary per repetition without cluttering the test body. ```java @BeforeEach void setUp(RepetitionInfo info) { dataset = Dataset.ofSize(info.getCurrentRepetition() * 100); } ``` **The gotcha every interviewer probes**: `@BeforeEach` runs for *every* test in the class, including plain `@Test` methods. A plain `@Test` has no repetition context, so when the resolver is asked for a `RepetitionInfo` there, it fails with a `ParameterResolutionException` at runtime. So a class that mixes `@Test` and `@RepeatedTest` methods **cannot** declare `RepetitionInfo` on a shared `@BeforeEach`. Two clean ways out: 1. Move the repeated tests into a `@Nested` inner class that has its own `@BeforeEach` taking `RepetitionInfo`, leaving the outer class's callbacks untouched. 2. Do not inject into the callback at all — take `RepetitionInfo` as a test-method parameter and derive what you need there. `RepetitionInfo` cannot be injected into `@BeforeAll` / `@AfterAll` — those are class-level, and by definition there is no single repetition in scope. ## Legitimate uses, and the smell Good uses: - **First-run-only work**: `if (info.getCurrentRepetition() == 1) warmUpJit();` to separate cold-start behaviour from steady state. - **Unique data per repetition**: deriving a distinct key/username/tenant id from the index so repetitions do not collide on a unique constraint in a shared database. - **Ramping**: growing an input size with the index to observe scaling within one test method. - **Diagnostics**: including the index in an assertion message or a `TestReporter` entry so a failure tells you exactly which run broke. The smell: heavy branching on `getCurrentRepetition()`. Once repetition 3 does something meaningfully different from repetition 2, you no longer have a repeated test — you have three different tests hidden behind an index, and `@ParameterizedTest` (which names its arguments and reports them) expresses that intent far better. A useful rule of thumb: if you find yourself writing `switch (info.getCurrentRepetition())`, you wanted parameterisation, not repetition. ## Interaction with the display name Note that `RepetitionInfo` and the `name` template are two separate views of the same data. The template placeholders `{currentRepetition}` / `{totalRepetitions}` control the *reported name*; `RepetitionInfo` gives the *test code* the same numbers. Use the template for readability of reports and the injected object only when the test body actually needs to branch or derive data. ## Version note `RepetitionInfo` has existed since JUnit 5.0. `getFailureThreshold()` arrived with the `failureThreshold` attribute in JUnit 5.10, so on older versions that method is simply not there.
- A class has one @RepeatedTest, one plain @Test, and a @BeforeEach that declares a RepetitionInfo parameter. What happens?The repeated test works, but the plain @Test fails with a ParameterResolutionException. @BeforeEach runs for every test in the class, and when it runs for a plain @Test there is no repetition context for the built-in resolver to supply. The fix is to move the repeated tests (with their own @BeforeEach) into a @Nested class, or to take RepetitionInfo as a parameter of the repeated test method itself.
- Can RepetitionInfo tell you how many repetitions have already failed?No. Its surface is getCurrentRepetition(), getTotalRepetitions() and getFailureThreshold(); it carries no history of previous outcomes. The engine tracks failures internally to honour failureThreshold, but that count is not exposed to the test. If you need cross-repetition state you must keep it yourself, typically in a static field or an extension's store — and then you are responsible for resetting it.
saying these in an interview costs you the question
- Treating getCurrentRepetition() as 0-based.
- Claiming RepetitionInfo can be injected into @BeforeAll.
- Not knowing that a shared @BeforeEach taking RepetitionInfo blows up for plain @Test methods in the same class.
- Expecting RepetitionInfo to report how many repetitions failed so far.
- Using a switch on the repetition index to run genuinely different scenarios instead of reaching for parameterisation.