Appearance
Mockito Architecture: How Mocks, Stubs and Spies Work
The previous chapter ended with a puzzle: on a spy, when(spy.multiply(4, 2)).thenReturn(100) runs the real multiply, while doReturn(100).when(spy).multiply(4, 2) does not. This chapter looks one level inside Mockito to explain it. You do not need these internals to use Mockito, but they make mocks, stubs and spies easy to reason about.
A typical Mockito test has four steps:
java
Calculator calculator = mock(Calculator.class); // 1. create a mock (or spy)
when(calculator.multiply(4, 2)).thenReturn(100); // 2. stub
int output = calculator.multiply(4, 2); // 3. invoke
verify(calculator).multiply(4, 2); // 4. verifyAll four, for mocks and spies alike, go through the same machinery.
Step 1: creating a mock or spy
java
Calculator mock = Mockito.mock(Calculator.class); // no real instance
Calculator spy = Mockito.spy(new Calculator()); // wraps a real instanceBoth go to a MockMaker, an interface with two main implementations. Both use Byte Buddy, a library for generating and changing Java bytecode at run time.
Subclass mock maker
Generates a subclass (a proxy, much like the ones Spring AOP creates) of your class and overrides every method so that it forwards the call to Mockito's interceptor. Conceptually:
java
class Calculator$MockitoMock extends Calculator { // generated at run time
MockMethodInterceptor mockitoInterceptor;
@Override
int multiply(int a, int b) {
return mockitoInterceptor.doIntercept(this, multiplyMethod, new Object[]{a, b},
() -> super.multiply(a, b)); // how to call the real method
}
}The interceptor receives the mock, the method, the arguments, and a real method handle: how to call the original code (here super.multiply), which a spy needs.
Limitation: final classes and final methods cannot be mocked, because they cannot be subclassed or overridden. Mockito then reports "Cannot mock/spy class ... - final class".
Inline mock maker (the default)
Creates no new class. It rewrites the bytecode of Calculator itself (through the JVM's instrumentation API, with a Java agent Mockito attaches at run time), inserting a hook at the start of each method. Conceptually:
java
class Calculator {
int multiply(int a, int b) {
if (isMock(this) && interceptionEnabled()) { // added by Mockito
return mockitoInterceptor.doIntercept(this, multiplyMethod, new Object[]{a, b}, realMethod);
}
return a * b; // the original code, unchanged
}
}Since the class itself is changed, final classes and final methods can be mocked too. Instances that are not mocks run the original code. The getClass() of an inline mock is plain Calculator; a subclass mock's is Calculator$MockitoMock$....
The inline mock maker has been the default since Mockito 5. Before that the subclass mock maker was the default, and mocking final classes needed the separate mockito-inline artifact.
(On recent JDKs Mockito prints a warning that it is "self-attaching to enable the inline-mock-maker"; the @InjectMocks chapter shows how to configure it as an agent instead.)
Why the "interception enabled" check? For a spy with no stub, Mockito calls the real method. With the inline mock maker, the real method is the instrumented method, so calling it would enter the hook again, reach the handler again, call the real method again: an infinite loop. So before calling the real method, Mockito turns interception off for that call; the original code runs; then interception is back on. The subclass mock maker does not need this, because it can call super.multiply() directly.
Whichever mock maker is used, every call on a mock or spy ends up in the interceptor, which hands it to the mock handler.
The mock handler and the invocation container
MockHandler(MockHandlerImpl) is the brain of Mockito. Every call on every mock, from any class, arrives here, and the handler decides what it means: a stubbing, a verification, or a normal call.InvocationContainer(InvocationContainerImpl) is the per-mock storage the handler uses: the stubs (for this method with these arguments, return or throw this) and the invocation history (which methods were called, with which arguments, how many times).
What the handler does with a call
Step 2: stubbing
when(mock.method(...)).thenReturn(...)
java
when(calculator.multiply(4, 2)).thenReturn(100);This is plain Java, so the argument calculator.multiply(4, 2) is evaluated first, before when is called. At that moment Mockito does not know a stubbing is coming. The call goes through the interceptor to the handler:
- Pending
doReturnanswer? No. - Verification? No.
- The call is recorded, and Mockito remembers it as the last call on a mock, held in a per-thread state ("ongoing stubbing").
- Stub found? No; this is the first time.
- Mock or spy? Mock: return the default for
int, which is0.
Then when(0) runs. It ignores the value passed in and takes the last recorded call (multiply(4, 2) on calculator). thenReturn(100) stores the stub in the invocation container and removes that call from the history, so it is not counted later. That is why verify(calculator, times(1)).multiply(4, 2) passes after one real call.
This is also why when() needs a call on a mock. With a plain object no call was recorded, and Mockito fails with "when() requires an argument which has to be 'a method call on a mock'".
On a spy, step 5 differs: no stub exists yet, so the default behavior for a spy applies, and the real method runs. Its result is thrown away by when, but any side effect (a print, a database write, an exception) has already happened. That is the answer to the puzzle.
doReturn(...).when(spy).method(...)
java
doReturn(100).when(calculator).multiply(4, 2);The order is reversed:
doReturn(100)creates the answer;.when(calculator)records that the next call oncalculatoris a stubbing and returns the mock itself..multiply(4, 2)is then called on it. The handler finds the pending answer at the first check, stores it as the stub formultiply(4, 2), and returns a dummy value. The real method never runs, mock or spy.
So doReturn is not faster; it is safe for spies, because the stub is registered before the method could run. On a mock, both styles are safe.
Step 3: invoking the stubbed method
java
int output = calculator.multiply(4, 2);Interceptor, handler: no pending answer, no verification. The call is recorded in the history (count: 1), a stub for multiply(4, 2) is found, and 100 is returned. For a spy the path is the same: the stub is found, so the real method does not run.
Step 4: verifying
java
verify(calculator).multiply(4, 2);verify(calculator) marks a pending verification and returns the mock. The following .multiply(4, 2) reaches the handler, which sees the verification and, instead of treating it as a call, checks the invocation history in the container: was multiply(4, 2) called, and as many times as required? If not, the test fails.
Next: Creating every mock by hand with Mockito.mock() gets repetitive. Chapter 16 covers the annotations @Mock, @Spy and @InjectMocks, how they connect to JUnit through MockitoExtension, and the exact rules @InjectMocks follows.
Check yourself
- What are the four steps of a typical Mockito test?
- What is a
MockMaker, and what are its two main implementations? - Why can the subclass mock maker not mock final classes, and why can the inline one?
- Which mock maker is the default in Mockito 5?
- What are the roles of the
MockHandlerand theInvocationContainer? - Why does the inline mock maker need to turn interception off when a spy calls the real method?
- During
when(mock.multiply(4, 2)), what doesmock.multiply(4, 2)return, and why? - How does
when(...)know which call to stub? - Explain why
when(...)on a spy runs the real method, butdoReturn(...).when(spy)does not. - Is
doReturnfaster thanwhen?
Answers
- Create the mock or spy, stub, invoke, verify.
- The component that creates mocks: the subclass mock maker (generates a subclass proxy) and the inline mock maker (rewrites the class's own bytecode). Both use Byte Buddy.
- A final class cannot be subclassed and a final method cannot be overridden. The inline mock maker changes the class itself, so it needs neither.
- The inline mock maker.
- The handler is the brain: it decides whether a call is a stubbing, a verification or a normal call, and what to return. The container stores the stubs and the invocation history for a mock.
- The real method is the instrumented method itself; calling it with interception on would loop back to the handler forever.
- The default value (
0forint): no stub exists yet, and it is a mock. - It ignores its argument and uses the last call made on a mock, which Mockito recorded per thread.
- With
when, the method call is evaluated before Mockito knows a stub is being defined; no stub exists, and a spy's default is to run the real method. WithdoReturn, the stub answer is registered first, so when the method is called the handler stores the stub without running the real method. - No. It is used because it does not run the real method, which matters for spies.