Mockito's spy(realObject) does not wrap the instance you hand it. Explain what it actually builds, and what that means for state changes made through the spy and for calls the real code makes on itself.
answer
- New instrumented instance + shallow field copy
- Original goes stale — discard the reference
- Real bodies run on the spy, so this == spy
- Self-calls hit stubs; delegatesTo does not
- final/private/static caveats
basics
~20 sMockito creates a new instrumented instance of the same class and shallow-copies the original's fields into it. Mutations through the spy therefore never reach the original object. Real method bodies execute on the spy, so this is the spy and self-calls are intercepted and stubbable.
solid answer
~60 s`spy(obj)` builds a **separate instrumented instance** of `obj`'s class and copies `obj`'s fields into it (a shallow copy — object references are shared, the referenced objects are not cloned). It is not a delegating wrapper. Two consequences matter in practice. **The original is frozen out.** State you mutate through the spy lands in the copy; asserting on the original object afterwards shows the state as of the moment `spy()` was called. Tests that keep a reference to the original and assert on it are a classic source of "my change disappeared". **Self-invocation is intercepted.** Because the real bodies run on the spy instance, `this` inside them *is* the spy. So if `process()` internally calls `this.validate()`, and you stubbed `validate()`, the stub wins. This is what makes partial mocking usable at all — and it is why a spy differs from a delegating double such as `mock(Foo.class, delegatesTo(real))`, where the delegate's internal calls bypass the double entirely. The copy is also best-effort: `final` fields may not copy cleanly, so prefer spying on an object built in the test over one wired elsewhere.
code
java · 10 lines@Test
void spyIsACopy() {
List<String> original = new ArrayList<>();
List<String> spied = spy(original);
spied.add("a");
assertEquals(0, original.size()); // untouched snapshot
assertEquals(1, spied.size());
}go deeper
Know the headline: the spy is a copy, not a wrapper, so changes through it do not show up on the original object.
Add that real method bodies run on the spy instance, which is why internal self-calls can be stubbed, and that the field copy is shallow.
Contrast spy with delegatesTo, name the final/private/static interception limits, and explain why self-call stubbing couples the test to the class's internal call graph.
Use it to argue an isolation policy: techniques that let tests assert on internal call structure raise refactoring cost across the suite, so bound where partial mocking is permitted.
## What spy() actually constructs `Mockito.spy(realObject)` is shorthand for creating a mock with two settings: the spied instance, and a default answer of "call the real method". Under the hood Mockito generates an instrumented type for the class (a Byte Buddy subclass with the default mock maker, or an instrumented version of the class itself with the inline mock maker), instantiates it **without running a constructor**, and then copies the fields of `realObject` into the new instance one by one via reflection. So you end up with **two objects**: the original you passed in, and the spy. They start with equal field values and then diverge. ## Consequence 1 — the original goes stale ```java List<String> original = new ArrayList<>(); List<String> spied = spy(original); spied.add("a"); original.size(); // 0 — the mutation went into the copy spied.size(); // 1 ``` Anyone holding a reference to `original` — including production code that was handed it before the test wrapped it — sees the pre-spy snapshot. In a test this shows up as an assertion on the wrong object; in a Spring-style setup it shows up as production code and test code operating on different instances. The rule of thumb: after `spy(x)`, throw away your reference to `x` and work exclusively with the spy. The copy is **shallow**. If the original held a reference to a `Map`, the spy holds the same `Map` reference, so mutations *through that map* are visible from both. Only the fields themselves are independent, not the objects they point to. This half-shared state is worth knowing when a test seems to leak in one direction but not the other. The copy is also **best-effort**. Mockito copies leniently and swallows failures rather than aborting, so a field it cannot write — `final` instance fields on some JVM configurations, fields blocked by module access rules — may silently arrive with its default value. A spy whose object was constructed elsewhere and passed in is therefore riskier than one you build inline in the test. ## Consequence 2 — self-invocation is intercepted This is the property that makes partial mocking work and the one most candidates get wrong. Because the real method bodies execute **on the spy instance**, `this` inside them refers to the spy. Any call the real code makes to another of its own public/protected methods passes through Mockito's interceptor: ```java class Job { void run() { if (enabled()) doWork(); } boolean enabled() { return readConfigFromDisk(); } void doWork() { /* ... */ } } Job job = spy(new Job()); doReturn(false).when(job).enabled(); job.run(); // real run() executes; its internal enabled() hits the stub -> doWork() skipped ``` Contrast this with a **delegating** double. `mock(Job.class, AdditionalAnswers.delegatesTo(realJob))` forwards each call to a genuinely separate object, so `realJob.run()` calls `realJob.enabled()` directly and your stub is bypassed. Same superficial goal, completely different interception semantics. (Delegation is the right tool when the real object cannot be subclassed, or when you deliberately want internal calls left alone.) Two limits on interception: `private` methods are never intercepted — they are not overridable and are invoked directly — and `static` methods need `mockStatic`. With Mockito 5's default inline mock maker, `final` classes and `final` methods *can* be instrumented; with the older subclass mock maker they could not, and stubbing a final method silently had no effect, which produced some memorably confusing test failures. ## Why this couples tests to internals Self-call interception is powerful and, for the same reason, brittle. A test that stubs `enabled()` to control `run()` is asserting on the class's **internal call graph**, not only its behaviour. Inline `enabled()` into `run()`, rename it, or make it private, and the test breaks though nothing observable changed. That is the real argument against leaning on spies: not that they are slow or exotic, but that they turn refactorings into test failures. ## Practical guidance - Build the object inside the test and spy it immediately: `Foo foo = spy(new Foo(dep));` — never keep and assert on the pre-spy reference. - Prefer injecting the spy into whatever consumes it, so production code and the test share one instance. - Remember the copy is shallow: shared mutable fields are still shared. - Do not rely on spying an object with `final` fields wired externally; the copy may not carry them. - When you need real behaviour but explicitly do **not** want self-calls intercepted, use `delegatesTo` instead of `spy`. - Keep the number of self-methods you stub at one, and treat two as a signal to extract a collaborator.
- How does spy() differ from mock(Foo.class, AdditionalAnswers.delegatesTo(realFoo))?A spy is one instrumented instance carrying copies of the original's fields, so real code runs on the spy and its internal self-calls are intercepted and stubbable. delegatesTo creates a mock that forwards each call to a genuinely separate real object, so that object's internal calls never come back through the mock and cannot be stubbed. Use delegation when the class cannot be subclassed or when you specifically want internal calls left alone.
- A test spies an object, mutates it, and then asserts on the reference it originally passed to spy(). Why does the assertion fail?Because spy() produced a separate instance and copied the fields; the mutation landed in the spy while the original kept its pre-spy state. The fix is to discard the original reference entirely and both mutate and assert through the spy, and to make sure production code was handed the spy rather than the original.
spy() is a body double who has memorised everything the original knew up to that moment — not a translator standing beside them relaying messages. Whatever the double learns afterwards, the original never hears.
saying these in an interview costs you the question
- Describing a spy as a wrapper that delegates to the object passed in
- Expecting mutations through the spy to be visible on the original
- Assuming the field copy is deep, so shared mutable state stays independent
- Believing self-invocation bypasses stubs the way it does with delegation
- Assuming private methods can be stubbed because self-calls are intercepted