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
-
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).
-
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.
-
Read release notes
- Skim Mockito 4.x release notes for breaking changes, deprecations, and new behaviors.
-
Add migration safety net
- Create a CI job that runs the test suite and reports failures per module so you can iterate.
Migration steps
-
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'
- For Maven:
-
Run tests and capture failures
- Run the entire test suite to see immediate breakages. Focus fixes on failing modules first.
-
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.
-
Address behavioral changes
- Update tests that relied on older mocking semantics (detailed below).
-
Clean up deprecated usages
- Replace deprecated APIs with recommended alternatives (for example, favoring Mockito.mock(Class.class, withSettings()) for advanced settings).
-
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'
- Maven:
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 { ... }
- JUnit 4:
Common pitfalls and concrete fixes
- 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().
- 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”.
- 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.
- 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.
- Spies executing real code unexpectedly
- Symptom: spies cause side effects when stubbing.
- Fix: replace when(…) with doReturn(…).when(…), or refactor to pure mocks.
- 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.
Leave a Reply