skip to content

Final Types & the Inline Mock Maker

Why classic subclass mocking can't touch final classes/methods, and how the inline mock maker uses bytecode instrumentation to do it — now the default, historically via the mockito-inline artifact. Also the home of the JDK dynamic-agent-loading warning story.

on this pageshow

questions

5

Older Mockito versions could not mock a final class or a final method. Explain the mechanism behind that limitation and what the inline mock maker does differently.

level: middleimportance: must knowfreq 58%

answer

  1. subclass maker = generated subclass, override methods
  2. final class / final method / private / static → unoverridable
  3. inline maker = agent + retransform, preamble in the real method
  4. Mockito 5 default = inline
  5. equals/hashCode and JVM-critical types still off-limits

basics

~20 s

The classic mock maker generates a runtime subclass and overrides methods, and Java forbids subclassing a final class or overriding a final method. The inline mock maker instead attaches a Java agent and rewrites the loaded class's own bytecode, so no subclass is needed.

solid answer

~50 s

Mockito's original **subclass mock maker** (ByteBuddy-based, formerly cglib) creates a mock by generating a subclass of the mocked type at runtime and overriding every method to route into Mockito. That mechanism inherits Java's rules: a `final` class cannot be subclassed, a `final` method cannot be overridden, `private` and `static` methods are not virtual, and the instance is created via Objenesis so no constructor runs. The **inline mock maker** takes a different route: it attaches a Java agent and uses the instrumentation API to *retransform the class itself*, splicing a dispatcher check into the top of each method body. Because it modifies the real class rather than deriving from it, `final` classes and `final` methods become mockable — and the same machinery underpins static and constructor mocking. Since Mockito 5, the inline maker is the default in `mockito-core`. Some things are still off-limits: `equals`/`hashCode`, and JVM-critical types the instrumentation refuses to touch.

code

java · 13 lines
java
public final class PricingRules {
    public final BigDecimal discountFor(String tier) {
        return BigDecimal.ZERO;
    }
}

@Test
void mocksFinalClassAndFinalMethod() {
    PricingRules rules = mock(PricingRules.class);
    when(rules.discountFor("GOLD")).thenReturn(new BigDecimal("0.15"));

    assertEquals(new BigDecimal("0.15"), rules.discountFor("GOLD"));
}

go deeper

for a junior

State that the old approach generates a subclass, which final types forbid, and that the inline maker rewrites the class instead.

for a middle

Explain the instrumentation mechanism, list what each maker can and cannot intercept, and know Mockito 5 changed the default.

for a senior

Read failures diagnostically — including the silent no-op when stubbing a final method under the subclass maker — and know the remaining exclusions.

for a principal

Tie the mock-maker choice to platform constraints (Android, native image, agent policy) and to whether the codebase should depend on mocking final third-party types at all.

## Two ways to build a mock A "mock maker" is Mockito's pluggable strategy for producing an object whose method calls can be intercepted. Two implementations matter. **Subclass mock maker** (`mock-maker-subclass`, the classic default up to Mockito 4). At mock creation time, ByteBuddy generates a new class that `extends` the mocked class (or `implements` the interface), overriding every non-final, non-private method with a body that forwards to Mockito's interception handler. An instance of that generated class is then allocated with Objenesis, which creates the object *without running any constructor*. The mock you hold is an instance of a synthetic subclass. **Inline mock maker** (`mock-maker-inline`). At startup Mockito attaches a Java agent to its own JVM (via ByteBuddy's agent, using the attach API) and registers a `ClassFileTransformer`. When a class is mocked, its bytecode is retransformed: each method body gains a preamble that asks a global dispatcher "is there an active mock/interception for this instance and method?" — if yes, control goes to Mockito; if no, the original body runs. The class identity does not change; there is no synthetic subclass. Mock *instances* are still created with Objenesis. ## Why the subclass maker cannot handle final Everything the subclass maker can intercept must be *virtually dispatchable and overridable*: - `final class` — the JVM rejects a subclass outright (`VerifyError`/`ClassFormatError` at generation time); Mockito reports "Cannot mock/spy final class". - `final method` — cannot be overridden, so calls run the real implementation. On a partial mock this is quietly confusing: your stubbing never takes effect. - `private method` — not virtual; a call is bound at compile time to the declaring class. - `static method` — no receiver, nothing to override. - `equals`/`hashCode` — Mockito needs these for its own bookkeeping and refuses to stub them under either maker. Because the inline maker edits the class's own methods, the first three restrictions disappear structurally: a final method still has a body, and the preamble goes into that body. That is also why static mocking and constructor mocking exist only with the inline maker. ## Where the boundary still is The inline maker is not unlimited: - `equals` and `hashCode` remain unmockable (Mockito's own identity bookkeeping depends on them); - primitive types have no methods to instrument; - a small set of JVM-critical classes is excluded because instrumenting them would destabilise the runtime — `Class`, `String` and similar core types; - a class that is already loaded can be retransformed, but the JVM forbids retransformation that changes schema (adding fields or methods), which is why the technique is a *preamble injection* rather than a rewrite; - native methods cannot be instrumented. Also, the historical restrictions that came from Objenesis instantiation remain: constructors do not run, so a mock's fields are null/zero regardless of mock maker. ## Version story in one line Mockito 2–4 shipped the subclass maker by default, with the inline maker available by opting in. Mockito 5.0 (2023) made the inline maker the default in `mockito-core`, and published a separate `mockito-subclass` artifact for environments where instrumentation is unavailable. The old `mockito-inline` artifact — which existed only to flip the default — was retired at that point. ## What this means when you read a failure "Cannot mock/spy because: final class" tells you unambiguously that the *subclass* maker is active — you are on Mockito 4 or earlier without opt-in, or on a build that explicitly selected `mock-maker-subclass`, or in an environment (Android, some native-image setups) where the inline maker is not usable. The fix is to change the mock maker, not to change the code under test. Conversely, a *silently ineffective* stubbing on a final method under the subclass maker produces no error at all — the real method simply ran — which is the harder version of the same diagnosis.

  • Under the subclass mock maker, what happens when you stub a final method on a spy?
    Nothing useful: the generated subclass cannot override the final method, so the call executes the real implementation and your stubbing never applies. Depending on how you wrote it, you may not even get an error — the test just behaves as if the stub were absent. That silent failure is one of the strongest arguments for the inline maker.
  • Do mocks created by the inline maker run the class's constructor?
    No. Instantiation is still done with Objenesis, which allocates the object without invoking any constructor, so all fields hold their JVM defaults. The inline maker changes how method calls are intercepted, not how instances are created. Any real method that relies on constructor-established state will still fail on a mock.
  • Why can Mockito mock static methods only with the inline mock maker?
    A static method has no receiver object, so there is nothing to subclass or override — the subclass maker has no hook at all. The inline maker instruments the declaring class's own bytecode, so a static method body can be given the same dispatcher preamble as an instance method. The interception is then scoped to a thread and a try-with-resources block.

saying these in an interview costs you the question

  • Saying Mockito can never mock final classes (true only for the subclass maker)
  • Claiming the inline maker creates a subclass with special permissions
  • Believing the inline maker runs constructors
  • Asserting equals/hashCode become mockable with the inline maker
  • Thinking the limitation was a Mockito API choice rather than a JVM-level constraint on subclassing

context

open as a page

How do you select which mock maker Mockito uses, and what happened to the mockito-inline artifact people used to add to their test dependencies?

level: middleimportance: should knowfreq 42%

basics

~20 s

Since Mockito 5 the inline maker is the default in mockito-core, so nothing is needed. Otherwise put a file mockito-extensions/org.mockito.plugins.MockMaker on the test classpath containing mock-maker-inline. The mockito-inline artifact existed only to flip that default and is retired; mockito-subclass now provides the old behaviour.

open as a page

Running a Mockito 5 test suite on JDK 21 or newer prints a warning that a Java agent has been loaded dynamically and that this will be disallowed by default in a future release. Where does that come from and how do you resolve it?

level: seniorimportance: should knowfreq 36%

basics

~20 s

Mockito's inline mock maker attaches ByteBuddy's agent to the running JVM at startup. JDK 21 warns about dynamic agent attachment because it is being phased out. Fix it by starting tests with -javaagent pointing at the byte-buddy-agent jar, or silence it with -XX:+EnableDynamicAgentLoading.

open as a page

A large Java test suite slows down and its heap usage climbs steadily after Mockito's instrumentation-based mock maker becomes the default. What costs does that mock maker introduce, and what would you do about them?

level: seniorimportance: nice to knowfreq 24%

basics

~20 s

Instrumenting each newly mocked class costs time on first use, and Mockito keeps per-mock state including recorded invocations and their arguments, so long-running suites accumulate memory. Clear it with Mockito.framework().clearInlineMocks() after each test, null out fields, and use the subclass maker for hot simple types.

open as a page

You own a Java codebase whose tests must also run in environments where bytecode instrumentation is unavailable or restricted. How do you decide how far to let the test suite depend on Mockito's instrumentation-based mocking?

level: principalimportance: nice to knowfreq 16%

basics

~20 s

Treat instrumentation-only mocking as a capability with a cost. Keep the inline maker as the default, but confine tests that need final-type, static or constructor mocking to a tagged subset, and make sure the core suite runs green under the subclass maker so restricted environments stay viable.

open as a page