I suspected the Mockito.spy() returns a wrapper around the copy of the parameter object, and the type of this wrapper is a generated sub type of the wrapped object. Then the spying functionality could be in the outer part of the wrapper, as generally in the case of a proxy object implementing some aspect.
However, it looks like not to be correct. The type of the wrapper is exactly the same as that of the wrapped object:
List<String> list = new ArrayList<>();
List<String> spyList = spy(list);
assertThat(spyList.getClass()).isSameAs(list.getClass());
But then where is the spying functionality implemented? If spyList is an ArrayList then how will Mockito be notified about the different calls to it?
Mockito achieves this by using retransformation ("HotSwap"), rewriting the bytecode of already-loaded classes so Mockito can intercept the behavior even of
finaland system classes. This is an artifact of the new MockitoMockMaker introduced by default in version 5.0+. This renders obsolete some previous subclass-centric StackOverflow answers about Mockito internals, though those are still relevant on Android and anywhere else you can't count on instrumentation running.The best place to read about the implementation is the org.mockito.internal.creation.bytebuddy.InlineDelegateByteBuddyMockMaker, rather than the org.mockito.internal.creation.bytebuddy.InlineByteBuddyMockMaker FQCN linked at the bottom of main Javadoc item 39. The last paragraph of that class's top-level Javadoc, emphasis mine:
It may also be helpful to skim ByteBuddy's Advice javadoc for context, as well as Mockito's MockMethodAdvice and MockMethodInterceptor to how Mockito achieves this magic at transformation-time and runtime respectively.
To reproduce this, use the following code:
As on JDoodle, running in Mockito 4.11.0 (org.mockito:mockito-core:4.11.0), this outputs:
But on Mockito 5.10.0 (org.mockito:mockito-core:5.10.0), this outputs: