skip to content

Under Mockito's strict stubbing, a test throws PotentialStubbingProblem in the middle of the code under test. What condition produces that exception, and why is it considered more useful than the older behaviour it replaced?

level: middleimportance: must knowfreq 48%

answer

  1. method stubbed + arguments match nothing = throw
  2. unstubbed method = no exception, default value
  3. message shows stubbing site vs actual call
  4. old behaviour: silent null, NPE far away
  5. fix data/matchers, not with lenient()

basics

~20 s

It fires when the code calls a mock method that IS stubbed, but with arguments matching none of its stubbings. Mockito fails right at that call, showing stubbed versus actual arguments. Previously the call silently returned null, so the test failed later with an unrelated NullPointerException.

solid answer

~60 s

`PotentialStubbingProblem` is strict stubbing's fail-fast check. Preconditions: 1. the method being called has at least one stubbing on that mock, and 2. the actual invocation matches none of them. Mockito throws immediately from inside the call, and the message prints both the stubbing (with file and line) and the actual arguments — usually the whole diagnosis. The old lenient behaviour returned the type default (`null`, `0`, `false`), so the test blew up much later, typically as a `NullPointerException` somewhere in the code under test, with a stack trace pointing at the *consumer* of the null rather than at the wrong argument. That is the single most common way Mockito wastes people's time. Typical causes: the test data does not match the stubbed argument; an object argument without `equals`, so `eq(...)` never matches; a `String`/`Long` boxing or ID mismatch; or production code now passes an extra/normalised value. The fix is almost always in the test data or matchers — widen to `any()` only when the argument genuinely does not matter, and never reach for `lenient()` to make it go away, because that just restores the silent `null`.

code

java · 23 lines
java
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
    @Mock UserRepository users;
    @InjectMocks OrderService service;

    @Test
    void mismatch() {
        when(users.findById(1L)).thenReturn(new User(1L));
        service.place(2L, "book"); // calls users.findById(2L) -> PotentialStubbingProblem
    }

    @Test
    void fixedByData() {
        when(users.findById(2L)).thenReturn(new User(2L));
        service.place(2L, "book");
    }

    @Test
    void fixedByMatcher() { // the id is not what this test asserts on
        when(users.findById(anyLong())).thenReturn(new User(2L));
        service.place(2L, "book");
    }
}

go deeper

for a junior

Recall that it means the code called a stubbed method with different arguments, and that you should compare the stubbed and actual values in the message.

for a middle

State both preconditions, contrast with the old silent-null behaviour, and name the common causes (data drift, missing equals, type mismatch).

for a senior

Use it as a diagnostic: explain when a mismatch signals a genuine production change or a vacuously passing legacy test, and where being specific vs generic in matchers belongs.

for a principal

Discuss it as evidence when justifying a strictness policy — mismatches found during migration are the concrete payoff, and swallowed exceptions in production catch blocks are the limit of the mechanism.

## The exact condition `PotentialStubbingProblem` is thrown during the test, at the moment a mock is invoked, when **both** are true: 1. the invoked method already has one or more stubbings registered on that mock, and 2. none of those stubbings' argument matchers match this invocation. If the method has no stubbings at all, nothing is thrown — the call returns the default value for its return type. That asymmetry is deliberate: a completely unstubbed collaborator method is a normal part of a focused test, whereas "stubbed, but not for these arguments" is nearly always a mistake. The message is unusually good: ``` PotentialStubbingProblem: Strict stubbing argument mismatch. Please check: - this invocation of 'findById' method: userRepository.findById(2); -> at com.example.OrderService.place(OrderService.java:41) - has following stubbing(s) with different arguments: 1. userRepository.findById(1); -> at com.example.OrderServiceTest.setUp(OrderServiceTest.java:33) ``` You get the call site in production code, the stubbing site in the test, and both argument lists. ## Why it replaced silent nulls Before strict stubbing, that mismatched call returned the type default. Consequences: - The failure surfaced later — often several frames deeper, as an NPE on `user.getName()`. - The stack trace pointed at the *victim* of the null, not the cause, so the natural first hypothesis ("the production code has a null bug") was wrong. - Worst case, the code tolerated the default (an `Optional.empty()`, a `0`, a `false`) and the test *passed* while asserting nothing meaningful. That last case is why enabling strict stubbing on a legacy suite sometimes uncovers real bugs: tests that were quietly passing on defaults start failing honestly. ## The usual causes **Test data drift.** The stubbing uses ID `1L`, the fixture builds the entity with `2L`. Fix the data. **Missing `equals`.** Stubbing `service.save(new Order(...))` matches by `equals`; a value object without `equals`/`hashCode` will never match a different instance. Fix: implement `equals`, use an `ArgumentMatcher`, or stub with `any(Order.class)` if the argument is not what the test is about. **Type or boxing mismatch.** `findById(1)` vs `findById(1L)`, or `String` vs `UUID` after a refactor. The compiler often accepts both through overloads or autoboxing. **Production behaviour changed.** The service now normalises the input (trims, lowercases, wraps in a request object) before calling the collaborator. The mismatch is telling you the contract moved; update the stubbing to the new contract rather than loosening the matcher. **Over-specific matchers.** A stubbing pinned to exact values in a test that is not about those values. Widening to `any()`/`anyLong()` is legitimate here — the rule of thumb is: be specific about arguments the test is asserting on, generic about the rest. ## What not to do Wrapping the stubbing in `lenient()` removes the check and restores the pre-strict behaviour: the call returns `null` and the test fails somewhere else, or passes vacuously. `lenient()` is for *unused* stubbings that are legitimately optional, not for mismatches. If you find yourself using it to silence a `PotentialStubbingProblem`, you are hiding a real disagreement between the test's assumption and the code's behaviour. Similarly, switching the class to `Strictness.LENIENT` disables the check everywhere in that class, which is a large price for one failing test. ## Where it does and does not apply - It requires a session with `STRICT_STUBS`: JUnit 5's `MockitoExtension` by default, `MockitoJUnitRunner.StrictStubs`, a `MockitoRule` configured with `STRICT_STUBS`, or a `MockitoSession`. The plain JUnit 4 `MockitoJUnitRunner` does **not** perform this check — it only detects unused stubbings at class level. - Mocks explicitly marked lenient are excluded. - Because it throws from inside the code under test, a `try/catch(Exception)` in production code can swallow it. If a strict test behaves oddly, remember `PotentialStubbingProblem` is a `RuntimeException` travelling through your own catch blocks; the unused-stubbing report at the end of the test is then your backstop signal. ## Interview framing State the two preconditions precisely (method is stubbed; arguments match no stubbing), contrast with the silent-`null` behaviour it replaced, and finish on the judgment: fix the arguments or matchers, never silence with `lenient()`.

  • If the code calls a mock method that has no stubbing at all, does strict stubbing throw?
    No. PotentialStubbingProblem requires the method to have at least one stubbing that failed to match. A method with no stubbings simply returns the default for its return type — null, 0, false — exactly as under lenient mode. That is why strict stubbing does not eliminate NullPointerExceptions from unstubbed collaborators.
  • A stubbing on save(new Order(...)) never matches even though the arguments look identical. Why?
    Argument matching uses equals(), so a value object without a proper equals/hashCode compares by identity and a different instance never matches. Either implement equals on the type, use an ArgumentMatcher or ArgumentCaptor to assert on the fields you care about, or stub with any(Order.class) when the exact argument is not what the test is about.

saying these in an interview costs you the question

  • Saying it fires for any call to an unstubbed method.
  • Silencing it with lenient() instead of fixing the arguments.
  • Claiming it is reported at the end of the test — it throws at the invocation.
  • Thinking the plain JUnit 4 MockitoJUnitRunner performs this check.
  • Widening every stubbing to any() as a reflex, which throws away the argument assertions the test needed.

context