Migrating from Mockito 4 to Mockito 5 Without Breaking Your Tests

The single change that trips up most teams on a Mockito 5 migration has nothing to do with API signatures. It’s that mockito-core now ships with the inline MockMaker as the default, replacing the subclass-based ByteBuddy maker that every Mockito 4 project quietly relied on. Tests that passed yesterday suddenly fail with Mockito cannot mock this class, or the JVM prints a WARNING: A Java agent has been loaded dynamically banner and exits non-zero on Java 21. Neither symptom is a real bug — both are Mockito 5 behaving exactly as documented — but they’ll stop your CI pipeline cold until you know what to change.

This mockito 5 migration guide walks through the upgrade the way it actually happens in a real Spring Boot or Jakarta EE codebase: dependency surgery first, then MockMaker fallout, then the smaller API deprecations, and finally the JDK and build-tool knobs you need to set so the inline agent loads cleanly on JDK 21 and later. I’ll stick to Mockito 5.11+, which is the line most teams are targeting in 2026 because it’s the first that runs without warnings on current LTS JVMs.

What actually changed between Mockito 4.11 and Mockito 5.x

Mockito 5.0.0 shipped in January 2023 and the release notes are blunt about what it breaks. The minimum supported Java version jumped from 8 to 11, the default MockMaker switched from subclass to inline, and several long-deprecated APIs finally disappeared. The v5.0.0 release page on GitHub lists the removals line by line, and the “What’s new in Mockito 5” wiki page explains the MockMaker switch in detail — both are the canonical sources and worth reading before you touch a pom.xml.

The practical consequences for a test suite written against Mockito 4:

  • Final classes and final methods are mockable by default — no more mockito-inline artifact, no more org.mockito.plugins.MockMaker resource file.
  • The standalone mockito-inline artifact still exists as an empty shim but is deprecated and will be removed; depending on it means you’re pinning yourself to a transitional state.
  • MockitoAnnotations.initMocks() is gone. You have to use MockitoAnnotations.openMocks(this) and close the returned AutoCloseable, or switch to MockitoExtension.
  • Matchers (the deprecated shim for ArgumentMatchers) was removed. If your codebase still imports org.mockito.Matchers, that’s a compile error now.
  • The legacy org.mockito.runners package is gone — MockitoJUnitRunner now only lives under org.mockito.junit.

None of these individually are hard. The pain comes from the MockMaker change, because it affects test behavior at runtime rather than at compile time, and because its failure mode looks different depending on which JDK you’re on.

Fix the dependencies before you touch any test code

Start in your build file. If you’re on Maven and your pom.xml has any of these, they need to go:

<!-- REMOVE these from Mockito 4 setups -->
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-inline</artifactId>
    <version>4.11.0</version>
    <scope>test</scope>
</dependency>

Replace them with a single mockito-core dependency at a version that matches your JDK. For Java 17 you can run anything from 5.0 up; for Java 21 you want 5.7.0 or newer, and for JDK 22/23 host agents you want 5.11.0+:

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>5.11.0</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <version>5.11.0</version>
    <scope>test</scope>
</dependency>

If you’re pulling Mockito transitively through spring-boot-starter-test, don’t override the version in both places — override it only in the Spring Boot BOM import or via spring-boot.mockito.version as a property. I’ve watched teams define Mockito 5 in <dependencyManagement> and then get Mockito 4 anyway because Spring Boot 3.1’s BOM wins the conflict resolution. Run mvn dependency:tree -Dincludes=org.mockito and confirm you see exactly one version before moving on.

Gradle users have the same situation. Explicitly pin the version in dependencies and then verify with ./gradlew dependencyInsight --dependency mockito-core --configuration testRuntimeClasspath. Transitive resolution through spring-boot-dependencies is the single most common reason a “Mockito 5 migration” ends up still running Mockito 4 in CI.

The MockMaker switch and what it breaks

In Mockito 4, the default MockMaker created a subclass of your target class using ByteBuddy. That meant final classes, final methods, static methods, and enums could not be mocked unless you added mockito-inline, which swapped in the instrumentation-agent-based MockMaker that can redefine loaded classes at runtime.

Mockito 5 flips the default. The inline MockMaker is now what you get from plain mockito-core, and the subclass maker is available as mock-maker-subclass if you specifically opt back into it. Most tests benefit from the change — you can finally mock a final service class without fighting the build — but there are two classes of failure you’ll hit.

Self-mocking and type-sensitive code

The inline MockMaker rewrites the bytecode of the actual target class rather than creating a subclass. Code that reflectively checks obj.getClass() == SomeClass.class will behave differently than code that worked with the old subclass mocks, because with inline mocking getClass() returns the real type rather than a generated SomeClass$MockitoMock$1234 subclass. That’s usually an improvement, but if you had tests that depended on the subclass shape — for example, checking mock.getClass().getSuperclass() — they’ll break.

More commonly, Hibernate lazy proxies and Spring AOP proxies interact oddly with inline-mocked beans. If you have a @MockBean that Spring wraps in a CGLIB proxy for transaction management, the resulting object is a CGLIB subclass of a mocked-inline target, and certain equals/hashCode interactions that used to be benign now throw. The fix is almost always to drop @MockBean in favor of constructor injection and plain @Mock, which is the direction Spring Boot 3.4+ is pushing anyway with @MockitoBean.

The Java agent warning on JDK 21+

JEP 451 in JDK 21 made dynamic agent loading print a warning, and a future release will disallow it entirely. The inline MockMaker loads its agent dynamically via ByteBuddyAgent.install(), so every test run on JDK 21 prints:

WARNING: A Java agent has been loaded dynamically (mockito-core-5.11.0.jar)
WARNING: If a serviceability tool is in use, please run with -XX:+EnableDynamicAgentLoading
WARNING: Dynamic loading of agents will be disallowed by default in a future release

The clean fix is to attach Mockito as a javaagent at startup, which is exactly what the Mockito team recommends in the release notes for 5.5.0 and later. In Maven Surefire:

<plugin>
  <artifactId>maven-surefire-plugin</artifactId>
  <version>3.2.5</version>
  <configuration>
    <argLine>-javaagent:${settings.localRepository}/org/mockito/mockito-core/5.11.0/mockito-core-5.11.0.jar -Xshare:off</argLine>
  </configuration>
</plugin>

In Gradle, resolve the jar path once and pass it through jvmArgs:

def mockitoAgent = configurations.testRuntimeClasspath.find {
    it.name.startsWith("mockito-core")
}
test {
    jvmArgs "-javaagent:${mockitoAgent}"
}

Once the agent is attached at launch, the warning is gone and you’re future-proofed against the JDK release where dynamic loading becomes an error rather than a warning.

Benchmark: Test Suite Execution Time: Mockito 4 vs 5
Performance comparison — Test Suite Execution Time: Mockito 4 vs 5.

API removals you’ll actually hit

Most of the deprecations that Mockito 5 finally cleaned up had been marked deprecated since the 2.x line. A few still come up constantly during migrations.

MockitoAnnotations.initMocks(this) is the big one. It’s been deprecated since 3.4.0 and is gone in 5.0. The replacement is openMocks, which returns an AutoCloseable so the framework can clean up inline mocks deterministically:

public class OrderServiceTest {
    @Mock private PaymentGateway gateway;
    @Mock private InventoryClient inventory;
    @InjectMocks private OrderService service;

    private AutoCloseable mocks;

    @BeforeEach
    void setUp() {
        mocks = MockitoAnnotations.openMocks(this);
    }

    @AfterEach
    void tearDown() throws Exception {
        mocks.close();
    }
}

Forgetting to close the returned AutoCloseable is a real memory leak with the inline MockMaker because the registered mocks stay pinned in the global registry. Over a long test run you’ll see metaspace pressure and eventually OutOfMemoryError: Metaspace. The simpler path is to stop writing @BeforeEach bootstrapping entirely and rely on MockitoExtension:

@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
    @Mock private PaymentGateway gateway;
    @InjectMocks private OrderService service;

    @Test
    void chargesCardOnCheckout() {
        when(gateway.charge(any(), anyLong())).thenReturn(Receipt.of("r-1"));
        service.checkout(Order.sample());
        verify(gateway).charge(any(), eq(499L));
    }
}

The extension manages the AutoCloseable for you and defaults to Strictness.STRICT_STUBS, which flags unused stubs as failures. Strict stubs were the default behavior even in Mockito 4 when you used the extension, so this is usually a non-change — but if your team had loosened it to LENIENT globally, revisit why. Most of the “lenient” usage I see in 4.x codebases is covering up tests that stub methods the production code no longer calls, which is exactly what strict mode exists to catch.

Two more compile-time removals to watch for: verifyZeroInteractions was replaced by verifyNoInteractions years ago and is finally gone, and org.mockito.Matchers (the old package name) is deleted entirely. Both are one-line find-and-replace fixes, but your IDE’s auto-import can silently pull in the wrong symbol if you’re not watching.

Running the migration: a concrete order of operations

Here’s the sequence I recommend for a codebase of any size. Do it on a feature branch, not trunk.

  1. Upgrade JUnit Jupiter to 5.10+ and Surefire to 3.2.5+ before touching Mockito. Older Surefire has its own JDK 21 issues that will confuse your diagnosis.
  2. Bump mockito-core and mockito-junit-jupiter to the same 5.x version. Remove any explicit mockito-inline dependency.
  3. Run mvn dependency:tree -Dincludes=org.mockito (or the Gradle equivalent) and confirm a single version.
  4. Run the test suite with no other changes and triage failures into three buckets: compile errors, MockMaker errors, and agent warnings.
  5. Fix compile errors mechanically: initMocksopenMocks, MatchersArgumentMatchers, verifyZeroInteractionsverifyNoInteractions.
  6. For MockMaker errors on specific classes, check whether the class is final and whether the test actually needs to mock it. Replacing an over-mocked final class with a real fake is usually cleaner than wrestling with the MockMaker.
  7. Add the -javaagent argument to Surefire/Gradle test tasks as shown above.
  8. Re-run. The suite should be green with no agent warnings in the log.

The single most common miss during step 6 is Kotlin interop. Kotlin classes and methods are final by default, and in Mockito 4 you were probably running mockito-inline or the mockito-kotlin wrapper to cope with that. On Mockito 5 the inline maker is already active, but mockito-kotlin versions below 5.0 still reference removed APIs. Upgrade mockito-kotlin to 5.2.1 or later when you bump core, or the Kotlin tests will fail with NoSuchMethodError on MockitoKt.mock.

Official documentation for mockito 5 migration guide
Official documentation — the primary source for this topic.

Android and Mockito: a separate path

Android projects are the one place the migration story changes shape. The Android runtime doesn’t support the instrumentation-based inline MockMaker, so org.mockito:mockito-android continues to use a subclass-based maker backed by dexmaker-mockito-inline for Robolectric and instrumented tests. The package still tracks Mockito core versions, but you can’t blindly drop mockito-android for mockito-core. Keep the Android artifact on the version train that matches your mockito-core and don’t add the -javaagent argument on device tests — it does nothing there and confuses the Gradle Android plugin’s test runner.

What to do if a specific class just refuses to mock

You’ll eventually hit a class that still throws Mockito cannot mock this class under the inline maker — usually something in the JDK itself (java.lang.ClassLoader, java.lang.Thread) or a class loaded by the bootstrap class loader. The inline maker can’t redefine bootstrap classes, and that’s not going to change. Don’t fight it. Wrap the offending JDK type behind your own interface, mock the interface, and inject the real implementation in production. It’s more code than PowerMockito.mockStatic ever was, but it’s the pattern the Mockito maintainers have been recommending since the 3.x line and it’s the reason PowerMock is effectively abandoned in 2026.

The one concession Mockito 5 does make is Mockito.mockStatic(), which has been stable since 3.4.0 and works with the inline maker. If you had PowerMock purely for static method mocking, you can delete PowerMock and use mockStatic directly — no bytecode-rewriting JUnit runner required.

The practical takeaway: the Mockito 5 upgrade is a two-file change — your build file and your Surefire/Gradle test configuration — followed by a mechanical sweep of deprecated API usages. Everything else is fallout from the MockMaker switch, and nearly all of that fallout is better fixed by deleting an unnecessary mock than by configuring your way around the new default. Run dependency:tree first, pin the -javaagent argument, and treat the first failing test as a signal rather than an obstacle.