Advanced Mockito: Stubbing, Spying, and Argument Captors Explained

Migrating Tests to Mockito 4: Best Practices and Common PitfallsMigrating your test suite to Mockito 4 can be rewarding: improved API stability, better Java 17+ support, and bug fixes. However, large codebases with many existing tests may run into incompatibilities or subtle behavioral changes. This article walks through a practical, step-by-step migration plan, highlights best practices to adopt during migration, and lists common pitfalls with concrete examples and fixes.


Why migrate to Mockito 4?

  • Long-term maintenance: Mockito 4 is the actively supported branch with bug fixes and compatibility updates.
  • Java compatibility: Improved support for modern Java versions (17+), modules, and new bytecode constraints.
  • Cleaner APIs: Deprecations and API refinements encourage better test practices (e.g., fewer static-heavy patterns).
  • Performance and stability: Internal improvements reduce flakiness in certain mocking scenarios.

Before you begin: prepare and plan

  1. Inventory tests

    • Identify the number of tests and which modules use Mockito.
    • Flag tests relying on internal or unsupported Mockito behaviors (reflection on Mockito internals, custom answers that depend on internal implementation details).
  2. Lock the build

    • Ensure you have a reproducible build environment (CI branch, consistent Maven/Gradle wrappers).
    • Pin other testing-related dependencies (JUnit, Hamcrest, AssertJ) to versions known to work with Mockito 4.
  3. Read release notes

    • Skim Mockito 4.x release notes for breaking changes, deprecations, and new behaviors.
  4. Add migration safety net

    • Create a CI job that runs the test suite and reports failures per module so you can iterate.

Migration steps

  1. Upgrade dependency

    • For Maven:
      
      <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>4.x.x</version> <scope>test</scope> </dependency> 
    • For Gradle:
      
      testImplementation 'org.mockito:mockito-core:4.x.x' 
  2. Run tests and capture failures

    • Run the entire test suite to see immediate breakages. Focus fixes on failing modules first.
  3. Fix compilation errors

    • Replace removed or relocated classes/APIs.
    • If you used Mockito’s internal classes (org.mockito.internal.*), switch to public APIs or rewrite tests.
  4. Address behavioral changes

    • Update tests that relied on older mocking semantics (detailed below).
  5. Clean up deprecated usages

    • Replace deprecated APIs with recommended alternatives (for example, favoring Mockito.mock(Class.class, withSettings()) for advanced settings).
  6. Add backported behavior when safe

    • For rare incompatibilities, you may add shims, but prefer updating tests to remain future-proof.

Key API changes and how to handle them

1) Stricter stubbing and unnecessary stubbing detection

Mockito 4 continues the push toward stricter testing by encouraging fewer irrelevant stubbings. If you enabled strictness (via MockitoJUnitRunner.Strictness or MockitoSession) you may see failures for stubbings that are never used.

Fix:

  • Remove unused when(…).thenReturn(…) stubbings.
  • Use doReturn/when for spies where necessary.
  • Use lenient() for legitimate but unused stubs:
    
    lenient().when(myMock.someMethod()).thenReturn(value); 

2) Spies: doReturn vs when on real methods

Calling when(spy.realMethod()) executes the real method. Prefer doReturn for stubbing spies:

// bad — executes real method when(spy.someMethod()).thenReturn(x); // good doReturn(x).when(spy).someMethod(); 

3) Final classes and methods

Mockito 2 required the inline mock maker for final classes; Mockito 4 continues to support inline mocking but ensure you have the mockito-inline artifact if you mock final types:

  • Add dependency:
    • Maven:
      
      <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-inline</artifactId> <version>4.x.x</version> <scope>test</scope> </dependency> 
    • Gradle:
      
      testImplementation 'org.mockito:mockito-inline:4.x.x' 

4) Java module system (JPMS) and reflective access

If your tests run under strict module rules (Java 9+), Mockito’s reflective access may require opening modules. Either:

  • Add –add-opens JVM flags in test runs, or
  • Use mockito-inline and ensure your test module allows reflective access to the code under test.

5) ArgumentCaptor and generics

Mockito improved type-safety in places; some raw-type captures may require explicit type tokens:

ArgumentCaptor<List<String>> captor = ArgumentCaptor.forClass((Class) List.class); 

Better: use helper methods or wrap assertions with proper generics.

6) Deprecated APIs removed or reworked

  • If your code used deprecated Matchers (org.mockito.Matchers), migrate to org.mockito.ArgumentMatchers.
  • Replace MockitoJUnitRunner with MockitoExtension (JUnit 5) if moving test platforms:
    • JUnit 4:
      
      @RunWith(MockitoJUnitRunner.class) public class MyTest { ... } 
    • JUnit 5:
      
      @ExtendWith(MockitoExtension.class) public class MyTest { ... } 

Common pitfalls and concrete fixes

  1. Tests failing due to unnecessary stubbings
  • Symptom: tests fail only under strictness with message about unused stubbings.
  • Fix: remove the stub or mark it lenient().
  1. Mockito not mocking final classes at runtime
  • Symptom: real constructor/method executed rather than mock.
  • Fix: add mockito-inline dependency or configure mock maker inline via mockito-extensions/org.mockito.plugins.MockMaker file containing “mock-maker-inline”.
  1. ClassCastException with deep stubbing / chained mocks
  • Symptom: runtime ClassCastException in chained calls.
  • Fix: avoid deep stubs; explicitly mock intermediate return types or use answer stubbing.
  1. Tests that rely on invocation order across multiple mocks
  • Symptom: nondeterministic failures.
  • Fix: use InOrder to assert order, or redesign tests to not depend on global order.
  1. Spies executing real code unexpectedly
  • Symptom: spies cause side effects when stubbing.
  • Fix: replace when(…) with doReturn(…).when(…), or refactor to pure mocks.
  1. Mixing JUnit 4 runner and JUnit 5 extension
  • Symptom: Mockito annotations not processed.
  • Fix: use the extension for JUnit 5 or keep JUnit 4 runner; don’t mix.

Migration checklist (practical)

  • [ ] Upgrade Mockito dependency (mockito-core or mockito-inline).
  • [ ] Run whole test suite in CI; record failing modules.
  • [ ] Replace org.mockito.Matchers with org.mockito.ArgumentMatchers.
  • [ ] Replace deprecated APIs; migrate to MockitoExtension if using JUnit 5.
  • [ ] Convert spy stubs to doReturn where necessary.
  • [ ] Add lenient() to legitimate unused stubs or remove them.
  • [ ] Add mockito-inline or mock-maker-inline for final types.
  • [ ] Address JPMS reflective access issues with –add-opens if needed.
  • [ ] Remove usages of org.mockito.internal.* classes.
  • [ ] Re-run tests and iterate until green.

Example fixes: code snippets

Bad spy stubbing causing real execution:

// This will call the real method when(spy.getConfig()).thenReturn(config); // Fix: doReturn(config).when(spy).getConfig(); 

Lenient stubbing:

lenient().when(myMock.optional()).thenReturn("fallback"); 

Mocking final class using mockito-inline:

  • Gradle:
    
    testImplementation 'org.mockito:mockito-inline:4.8.0' 

When to postpone migration

  • Large monolithic test suites with frequent releases and no dedicated QA window — postpone until you can allocate time for triage.
  • If many tests rely on internal Mockito behavior or extensive custom answers — plan refactor first.
  • When third-party libs you mock are incompatible with mock-maker-inline and you can’t change them.

Long-term best practices (post-migration)

  • Prefer composition and explicit test doubles over heavy spying. Spies often lead to brittle tests.
  • Keep stubbings tight and local to the test that uses them.
  • Use ArgumentCaptor and focused assertions rather than broad verifications.
  • Prefer MockitoExtension (JUnit 5) for clearer lifecycle management.
  • Avoid deep stubs; mock intermediate collaborators explicitly.
  • Keep Mockito and test framework dependencies up to date on a regular cadence.

Summary

Migrating to Mockito 4 is mostly straightforward but can surface issues related to stricter stubbing, spy behavior, final-type mocking, JPMS reflective access, and deprecated APIs. Triage failures module-by-module, prefer code fixes that make tests clearer and less brittle, and adopt Mockito’s recommended patterns (doReturn for spies, lenient for necessary unused stubs, mockito-inline for final classes). With a staged approach and the checklist above, you can migrate reliably while improving test quality.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *