skip to content

Strictness & Mocking Hygiene

Mockito's built-in test-quality police — strict stubbing that fails on unused or mismatched stubs — plus the judgment layer: what deserves a mock at all and how to spot overmocked tests. Interviewers use this area to separate tool users from engineers who keep test suites honest.

on this pageshow

explore

questions

16

A Java unit test fails with Mockito's UnnecessaryStubbingException even though every assertion passed. What exactly triggers that exception, when is it reported, and how should you fix it?

level: juniorimportance: must knowfreq 62%

answer

  1. stubbing declared, never matched by a call
  2. reported after the test (per method under JUnit 5 extension)
  3. usual culprit: fat @BeforeEach used by some tests
  4. matcher mismatch can also leave it unused
  5. fix = delete/move; lenient() is the last resort

basics

~20 s

It means a stubbing you declared was never matched by any call during the test. Under strict stubbing Mockito reports it after the test, pointing at the stubbing's line. The fix is usually to delete the stubbing or move it into the test that actually needs it — not to switch strictness off.

solid answer

~60 s

`UnnecessaryStubbingException` says: *you told a mock what to return, and nothing ever asked*. It is raised by strict stubbing (the default for Mockito's JUnit 5 extension) at the end of the test, and the message names the file and line of the unused stubbing. Common causes: - The stubbing is simply dead — leftover from a refactor. - It lives in a shared `@BeforeEach` and only some tests exercise that path; with the JUnit 5 extension the check is per test method, so the others fail. - Production code changed and no longer calls that collaborator — the test is stale and its assertions probably need rethinking, not just a deleted line. - The stubbing's matchers do not match the real call, so a *different* stubbing or the default answer served it. Fixes, in order of preference: delete it; move it into the tests that use it; extract a helper that stubs on demand; and only then relax with `lenient()` on that stubbing or `@Mock(lenient = true)` on that mock.

code

java · 23 lines
java
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

    @Mock UserRepository users;
    @InjectMocks OrderService service;

    @BeforeEach
    void setUp() {
        when(users.findById(1L)).thenReturn(new User(1L)); // unused by countAll() -> UnnecessaryStubbingException
    }

    @Test void placesOrder() { service.place(1L, "book"); }
    @Test void countAll()   { service.count(); }
}

// Fix A: move it into the test that needs it
@Test void placesOrder() {
    when(users.findById(1L)).thenReturn(new User(1L));
    service.place(1L, "book");
}

// Fix B: keep the fixture, relax only that mock
@Mock(lenient = true) UserRepository users;

go deeper

for a junior

Say plainly that a stubbing was never called, and that the fix is to delete it or move it into the test that needs it.

for a middle

Add the per-method vs per-class reporting difference and the matcher-mismatch cause, and name the narrow leniency escape hatches.

for a senior

Treat it as a fixture-design signal: distinguish dead stubbings from stale tests where production behaviour changed, and argue against blanket leniency.

for a principal

Frame it as a suite-health signal and discuss how you keep teams fixing causes instead of silencing symptoms as the codebase grows.

## What the exception means A *stubbing* is a recorded instruction on a mock — `when(repo.findById(1L)).thenReturn(user)`. Mockito tracks whether each stubbing was ever *realised*, i.e. matched by an actual invocation on that mock. Under `Strictness.STRICT_STUBS`, any stubbing never realised by the end of the test produces `UnnecessaryStubbingException`, with a message like: ``` Unnecessary stubbings detected. Clean up unnecessary stubbings: 1. -> at com.example.OrderServiceTest.setUp(OrderServiceTest.java:34) ``` The assertions passing is not a contradiction: the test verified what it verified, and separately left an instruction nobody used. ## When it is reported The scope depends on the harness: - **JUnit 5 `MockitoExtension`** — a session per test method, so the check runs after *each* test. A `@BeforeEach` stubbing used by only some tests fails the others. - **JUnit 4 `MockitoJUnitRunner`** (the plain, `Strict` variant) — collects unused stubbings across the whole class and reports once at the end, so a `@Before` stubbing used by any test in the class is accepted. - **`MockitoJUnitRunner.StrictStubs`** / a `MockitoSession` with `STRICT_STUBS` — per-test behaviour like the extension. That scope difference is why a suite can go green on JUnit 4 and light up after a JUnit 5 migration. ## The four causes, and the right fix for each **1. Dead stubbing.** Left behind by a refactor; nothing calls it any more. Delete it. This is the case the exception exists for, and it is the majority. **2. Shared setup used by a subset of tests.** A fat `@BeforeEach` stubs five collaborators so that the three tests that need them are shorter. The real defect is fixture design. Preferred fixes: move each stubbing into the tests that need it; or extract intention-revealing helpers (`givenUserExists(1L)`) that tests call explicitly. If the fixture is genuinely shared and optional, mark that one mock `@Mock(lenient = true)` with a comment. **3. Stale test.** Production code stopped calling the collaborator. Deleting the stubbing makes the build green but leaves a test that no longer asserts what its name claims. Read the production change first, then decide whether the test should assert something new or be deleted along with the behaviour. **4. Matcher mismatch.** You stubbed `findById(1L)` but the code calls `findById(2L)`, or you stubbed `eq(user)` for an object whose `equals` you did not implement. The stubbing is unused because it never matched. Under strict stubbing this usually shows up first as `PotentialStubbingProblem` at the call site; if the method has other stubbings, or the call happened in a path that swallowed the result, you may see the unused-stubbing report instead. The fix is in the matchers or the test data, never `lenient()`. ## Why not just turn strictness off Switching the class to `@MockitoSettings(strictness = Strictness.LENIENT)` makes the message disappear and also disables argument-mismatch detection for every mock in the class. You trade a two-minute cleanup for permanently worse diagnostics: the next wrong-argument bug in that class will surface as a `NullPointerException` in unrelated code. Class-wide leniency is defensible as a temporary, commented migration marker; it is not a fix. ## Reading the message well The stack line points at the *stubbing*, not at the failure. If several stubbings are unused, Mockito lists them all, which is often the fastest way to see that one shared fixture is the source. If the exception appears only when the whole class runs and not for a single test, suspect state shared across tests (a static mock, a mock field initialised once) rather than the individual test. ## A subtlety about verification A stubbing counts as used when the mock is *called* in a matching way — not when you `verify()` it. Verifying a call that never happened fails with a verification error instead. Conversely, under strict stubbing a call that matched a stubbing is treated as already verified, so `verifyNoMoreInteractions` will not force you to verify stubbed calls. ## Interview framing Say what the exception means in one sentence, name the shared-`@BeforeEach` case as the usual real-world cause, and make clear that your first instinct is to delete or relocate the stubbing rather than to relax strictness.

  • Why does the same test class pass under JUnit 4's MockitoJUnitRunner but fail with UnnecessaryStubbingException under JUnit 5's MockitoExtension?
    The scope of the check differs. The plain JUnit 4 runner collects unused stubbings across the entire class and reports once at the end, so a @Before stubbing used by any single test counts as used. The JUnit 5 extension opens a session per test method, so every test that does not exercise that stubbing fails individually.
  • Does calling verify() on a stubbed method count as using the stubbing?
    No. A stubbing is marked used when the mock is actually invoked in a way that matches it during the test. verify() only asserts afterwards that such an invocation happened — if it did not, you get a verification failure rather than a satisfied stubbing. So you cannot silence UnnecessaryStubbingException by adding a verify().

saying these in an interview costs you the question

  • Fixing it by switching the class to LENIENT instead of removing or relocating the stubbing.
  • Thinking the exception means an assertion failed or the mock was never created.
  • Believing adding a verify() marks the stubbing as used.
  • Assuming it is always reported at the end of the class — under the JUnit 5 extension it is per test method.
  • Deleting the stubbing without checking whether the test went stale because production code changed.

context

open as a page

When you write a unit test for a service class that has several collaborators, how do you decide which of them to replace with Mockito mocks and which to construct for real?

level: juniorimportance: must knowfreq 70%

basics

~20 s

Mock the boundary: collaborators that leave the process or are slow, nondeterministic or hard to set up (repositories, HTTP clients, clocks, message publishers). Construct everything else for real: value objects, DTOs, enums, collections, pure helpers. Real is the default; a mock needs a reason.

open as a page

In Mockito every collaborator you build with Mockito.mock() is called a "mock". When you write when(...).thenReturn(...) versus when you write verify(...), which test-double role is that object actually playing, and why does it matter which one a given test uses?

level: middleimportance: must knowfreq 56%

basics

~20 s

Mockito.mock() is only a mechanism. Stubbing with when/thenReturn puts the object in a stub role: it supplies input and your assertion checks the result. verify() puts it in a mock role: the call itself is the thing being asserted. Pick one role per collaborator.

open as a page

Mockito's Strictness enum has three values: LENIENT, WARN and STRICT_STUBS. What does each one change about how a test behaves, and why was strict stubbing introduced?

level: middleimportance: must knowfreq 58%

basics

~20 s

LENIENT does no stubbing checks (old Mockito 1.x behaviour). WARN only logs hints about unused or mismatched stubbings. STRICT_STUBS fails the test: a stubbing that was never used is an error, and calling a stubbed method with arguments matching no stubbing fails at the call site instead of returning null.

open as a page

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%

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.

open as a page

You create a collaborator with Mockito.mock(SomeService.class) and never stub anything on it. What do its methods return, and what role is that unstubbed object playing in the test?

level: juniorimportance: should knowfreq 48%

basics

~20 s

Nothing throws. Mockito's default answer returns null for object types, 0/false for primitives, and empty collections or Optional.empty() for those types. If the test never uses those values, the object is just a placeholder to satisfy a constructor; if the code consumes them, it is a stub returning defaults.

open as a page

In a JUnit 5 test that uses Mockito's @MockitoSettings annotation, how do you change the strictness for a test class, what scope does that choice have, and how does it interact with per-mock leniency?

level: middleimportance: should knowfreq 40%

basics

~20 s

Put @MockitoSettings(strictness = Strictness.LENIENT) on the test class; it implies @ExtendWith(MockitoExtension.class), so you do not need both. It sets the level for every mock in that class. Prefer narrower tools first: @Mock(lenient = true) or lenient() on a single stubbing.

open as a page

Many teams follow the guideline 'do not mock types you do not own'. What failure is that guideline trying to prevent, and how do you test code that talks to a third-party library if you cannot mock its classes?

level: middleimportance: should knowfreq 45%

basics

~20 s

A mock of a third-party class encodes your guess about how that library behaves, and Mockito cannot check the guess. The test passes while production fails. Instead wrap the library in a thin interface you own, mock that in unit tests, and cover the wrapper with one real integration test.

open as a page

Mockito has no API that produces a working in-memory implementation of a dependency — every double it builds is a proxy driven by recorded answers. When a test needs a dependency that stays consistent across many calls, such as an in-memory store, how do you get one, and what changes compared with chains of when(...).thenReturn(...)?

level: seniorimportance: should knowfreq 34%

basics

~20 s

You write it yourself — a small class implementing the interface over a HashMap — or use a ready-made in-memory tool. Mockito can only return canned answers per call; a hand-written implementation keeps state, so a save followed by a lookup works without scripting every interaction.

open as a page

Mockito's spy() wraps a real object so that some methods run their real implementation while others are stubbed. In test-double terms, what kind of double is that, and what practical hazards come with using one?

level: seniorimportance: should knowfreq 42%

basics

~20 s

A spy is a partial double: real behaviour plus selective overrides — closer to a hand-written partial fake than to a pure mock, though it records calls so verify() also works. Hazards: when(spy.x()) actually executes the real method (use doReturn), the spy is a copy of your object, and final or static methods are not intercepted.

open as a page

Which strictness applies by default when mocks are managed by Mockito's JUnit 5 extension (@ExtendWith(MockitoExtension.class)), by the JUnit 4 MockitoJUnitRunner, and when you simply call Mockito.mock(...) with no runner or extension at all?

level: seniorimportance: should knowfreq 42%

basics

~20 s

The JUnit 5 MockitoExtension defaults to STRICT_STUBS. The plain JUnit 4 MockitoJUnitRunner defaults to its Strict variant, which reports unused stubbings once per test class but not argument mismatches; MockitoJUnitRunner.StrictStubs gives full strict stubbing. Bare Mockito.mock(...) with no harness enforces nothing.

open as a page

Mockito offers lenient() on a single stubbing and a lenient setting on a whole mock. How do these differ in scope, and when is suppressing a strict-stubbing failure legitimate rather than a sign of a bad shared fixture?

level: seniorimportance: should knowfreq 36%

basics

~20 s

lenient().when(...) exempts one stubbing; @Mock(lenient = true) or withSettings().lenient() exempts every stubbing on that mock. Suppression is legitimate for a genuinely optional shared fixture stub. It is a smell when a fat setup block stubs everything so tests can be short — fix the fixture instead.

open as a page

What are the practical signs that a Mockito test is over-mocked — for example a mock whose method returns another mock, or a test that fails on every refactoring even though behaviour did not change — and how do you fix each one?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Two big smells: mocks returning mocks (or RETURNS_DEEP_STUBS), meaning the test knows an object graph the code should not navigate; and mirror-the-implementation tests that stub and verify every call, so any refactoring breaks them. Fix by passing the leaf object the code needs, and by asserting outcomes instead of every interaction.

open as a page

Mockito exposes a MockitoSession API via Mockito.mockitoSession(). What is it for, how do you drive one by hand, and what breaks if you forget to finish it?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

MockitoSession is the mechanism behind the JUnit runner, rule and extension: it initialises mocks, applies a strictness level, and validates stubbing hygiene when finished. You build it with Mockito.mockitoSession().initMocks(this).strictness(...).startMocking() and must call finishMocking() afterwards, or the checks never run and mock state leaks into the next test.

open as a page

You take over a large legacy Java test suite where Mockito mocks are configured leniently and stubbing hygiene has drifted. How would you decide on a strictness policy and roll it out without stalling delivery?

level: principalimportance: nice to knowfreq 20%

basics

~20 s

Target STRICT_STUBS as the default and treat leniency as a tracked exception. Roll out per module: switch to WARN, fix what the hints reveal, then flip to STRICT_STUBS with narrow lenient() escapes where a shared fixture genuinely justifies it. Enforce the harness in review so new tests start strict.

open as a page

Across a large test suite, when would you invest in writing and maintaining a hand-rolled in-memory implementation of a collaborator such as a repository, instead of configuring Mockito mocks in each test that needs it?

level: principalimportance: nice to knowfreq 32%

basics

~20 s

Invest when the same collaborator is stubbed in dozens of tests, when scenarios need consistent behaviour across calls (save then read back), or when stubbings keep encoding rules. A shared in-memory implementation removes duplicated setup and holds invariants once; the cost is maintaining it and defending its fidelity.

open as a page