skip to content

In an older Java codebase you find one test class using @RunWith(MockitoJUnitRunner.class) and another declaring a MockitoRule field. What does each do, why would a team pick the rule, and what happens to both when the class is moved to JUnit 5?

level: middleimportance: nice to knowfreq 26%

answer

  1. runner and rule = JUnit 4 only
  2. one runner per class -> rules compose
  3. MockitoJUnitRunner is strict; .Silent and .StrictStubs variants
  4. validateMockitoUsage after each test
  5. JUnit 5 ignores both silently -> null mocks

basics

~20 s

Both are JUnit 4 Mockito integrations: they initialize annotated mocks, validate Mockito usage after each test, and by default detect unused stubbings. The rule exists because a class can have only one runner, so the rule leaves that slot free for another runner. Under JUnit 5 both are ignored silently, leaving mocks null.

solid answer

~40 s

`@RunWith(MockitoJUnitRunner.class)` replaces JUnit 4's default runner. It initializes `@Mock`/`@Spy`/`@Captor`/`@InjectMocks` before each test, calls `validateMockitoUsage()` afterwards, and since Mockito 2 the plain `MockitoJUnitRunner` is the strict variant that reports unused stubbings; `MockitoJUnitRunner.Silent` disables that, and `MockitoJUnitRunner.StrictStubs` adds argument-mismatch detection. `@Rule public MockitoRule rule = MockitoJUnit.rule();` does the same work as a rule rather than a runner. That matters because JUnit 4 allows exactly one runner per class: if the class already needs a parameterized or Spring runner, the rule is the only way to also get Mockito's integration. `MockitoJUnit.rule().strictness(...)` tunes strictness the same way. On JUnit 5, `@RunWith` and JUnit 4 `@Rule` fields are simply not understood — no error, no initialization — so the class must switch to `@ExtendWith(MockitoExtension.class)` or `openMocks`.

code

java · 9 lines
java
@RunWith(Parameterized.class)          // runner slot already taken
public class PricingTest {

    @Rule
    public MockitoRule mockito = MockitoJUnit.rule()
            .strictness(Strictness.STRICT_STUBS);

    @Mock TaxService taxService;       // still initialized, via the rule
}

go deeper

for a junior

Recognize both as the old JUnit 4 way to initialize Mockito annotations and know the JUnit 5 replacement is the extension.

for a middle

Explain the one-runner-per-class constraint that motivates the rule, plus usage validation and the strictness variants.

for a senior

Own the migration: the silent-ignore failure mode under Jupiter, the annotation-to-extension mapping, and enforcing consistent JUnit imports so hybrids cannot appear.

for a principal

Treat it as a fleet-wide migration problem — automated rewrite plus a build-level ban on JUnit 4 imports in migrated modules, so a class cannot silently lose its mock initialization.

## The runner JUnit 4 runs each test class through a *runner*. `@RunWith(MockitoJUnitRunner.class)` swaps in Mockito's runner, which wraps the standard behaviour with three services: 1. **Initialization** — before each test method it processes the Mockito annotations on the test instance: `@Mock`, `@Spy`, `@Captor`, then `@InjectMocks`. 2. **Usage validation** — after each test it calls `Mockito.validateMockitoUsage()`, which surfaces structurally broken usage (an unfinished `when(...)`, a `verify(mock)` with no method call after it) *in the test that caused it* rather than in some later, unrelated test. 3. **Strictness** — since Mockito 2, `MockitoJUnitRunner` is an alias for the strict runner: stubbings that no test used are reported as unnecessary. Variants: `MockitoJUnitRunner.Silent` (no strictness — the old Mockito 1 behaviour, used when migrating a legacy class), and `MockitoJUnitRunner.StrictStubs` (adds `PotentialStubbingProblem` for argument mismatches, matching what the JUnit 5 extension does by default). ## The rule ```java @Rule public MockitoRule mockito = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS); ``` A JUnit 4 class may have **only one** `@RunWith`. If the class already needs `Parameterized`, a Spring runner, or a vendor runner, the Mockito runner slot is taken — and that is precisely the situation the rule solves, since a class may declare many rules. The rule performs the same initialization, validation and strictness policy as the runner, and `MockitoJUnit.rule()` is the entry point. There is also `MockitoJUnit.testRule()` for the `TestRule` shape and `MockitoJUnit.collector()` for gathering multiple verification failures into one report. Style-wise, many JUnit 4 codebases standardized on the rule for exactly this composability reason: rules stack, runners do not. ## What happens under JUnit 5 JUnit Jupiter has no concept of runners or of JUnit 4 rules. A `@RunWith` annotation left on a Jupiter test class is not honoured and **not reported** — the class runs, the annotated fields are never populated, and the first use throws `NullPointerException`. A JUnit 4 `@Rule` field is likewise inert; it is just a field holding an object nobody consults. This is one of the most common migration bugs, because the code still compiles (junit 4 is often still on the test classpath transitively) and the failure looks unrelated to the migration. The migration is mechanical: | JUnit 4 | JUnit 5 | |---|---| | `@RunWith(MockitoJUnitRunner.class)` | `@ExtendWith(MockitoExtension.class)` | | `@RunWith(MockitoJUnitRunner.Silent.class)` | `@ExtendWith(MockitoExtension.class)` + `@MockitoSettings(strictness = LENIENT)` | | `MockitoJUnit.rule()` (needed because another runner was present) | just add the extension — Jupiter composes extensions freely, so the conflict disappears | | `MockitoJUnit.rule().strictness(STRICT_STUBS)` | the extension's default | Note the third row: the entire reason the rule existed evaporates under Jupiter, because extensions compose. A migrated class typically ends up with several `@ExtendWith` annotations and no rules at all. ## The junit-vintage caveat A project can execute genuinely-JUnit-4 classes on the JUnit Platform through the vintage engine; those classes keep working with the runner or rule, because the vintage engine really does run them as JUnit 4. The failure mode is a *hybrid* class — Jupiter `@Test` imports with a JUnit 4 runner annotation still attached — which is why consistent imports are the thing to check first when mocks are unexpectedly null. ## Interview framing Describe both as the JUnit 4 integrations, explain the one-runner-per-class constraint as the rule's reason to exist, list what they provide (initialization, usage validation, strictness with Silent/StrictStubs variants), and finish with the migration fact that matters operationally: JUnit 5 ignores both silently, so the symptom is a null mock rather than a configuration error.

  • A migrated class still has @RunWith(MockitoJUnitRunner.class) and Jupiter @Test methods, and the mocks are null. Why is there no configuration error?
    Jupiter simply does not read `@RunWith`; unknown annotations are ignored, so the class runs as an ordinary Jupiter test with nothing initializing the annotated fields. The first use of a mock then throws NullPointerException far from the real cause. Replacing the runner with `@ExtendWith(MockitoExtension.class)` fixes it, and a lint rule banning JUnit 4 imports in Jupiter modules prevents recurrence.
  • What is MockitoJUnitRunner.Silent for?
    It restores Mockito 1's non-strict behaviour, where unused stubbings do not fail the test. Its legitimate use is temporary: adopting Mockito 2+ on a large legacy suite without an immediate flood of unnecessary-stubbing failures. Left permanently it discards a genuinely useful signal, so the intent is to migrate classes off it stub by stub.

saying these in an interview costs you the question

  • Believing @RunWith(MockitoJUnitRunner.class) works on JUnit 5
  • Not knowing why the rule exists (only one runner per JUnit 4 class)
  • Thinking the plain MockitoJUnitRunner is non-strict in Mockito 2+
  • Combining a Mockito runner with the JUnit 5 extension on the same class
  • Assuming a JUnit 4 @Rule field is honoured by Jupiter because it compiles

context