While working with some legacy tests, I recently discovered some unexpected behavior of Mockito and its spies. Consider the following class (especially note the anonymous nested implementation of SomeInterface
)
public class ClassUnderTest {
private String name = "initial value";
private final SomeInterface impl = new SomeInterface() {
@Override
public void foo(String name) {
// the following call "unwraps" the spied object and directly calls internalFoo on the "raw" object but NOT on
// the spy (method is called on the "toBeSpied" object from testObjInstantiation and not on the "spy" instance)
internalFoo(name);
}
};
private final class SomeClass {
private void foo(String name) {
// works as expected when using a nested class (called on the spy)
internalFoo(name);
}
}
public void foo(String name) {
impl.foo(name);
}
public void bar(String name) {
internalFoo(name);
}
public void baz(String name) {
new SomeClass().foo(name);
}
public String getName() {
return name;
}
private void internalFoo(String name) {
this.name = name;
}
private interface SomeInterface {
void foo(String name);
}
}
Furthermore consider the following test:
@Test
void testObjInstantiation() {
final var toBeSpied = new ClassUnderTest();
final var spy = Mockito.spy(toBeSpied);
spy.bar("name set on spy via bar");
Assertions.assertEquals("name set on spy via bar", spy.getName());
spy.baz("name set on spy via baz");
Assertions.assertEquals("name set on spy via baz", spy.getName());
spy.foo("name set on spy via foo");
Assertions.assertEquals("name set on spy via foo", spy.getName()); // this fails Expected: name set on spy via foo Actual: name set on spy via baz
}
I would expect all assertions to succeed. However, the last one fails. The reason for this is that spy.foo
uses the "indirection" via the SomeInterface
implementation (impl
member). At this point the spied object is "unwrapped". internalFoo
which is called from impl
is not called on the spy anymore but on the "raw" object. Basically it is called on the toBeSpied
instance from the test case and not on the spy
instance.
When using a nested class, everything works as expected (see ClassUnderTest.baz
which instantiates a SomeClass
object).
Consider the following test:
@Test
void testClassInstantiation() {
final var spy = Mockito.spy(ClassUnderTest.class);
spy.bar("name set on spy via bar");
Assertions.assertEquals("name set on spy via bar", spy.getName());
spy.baz("name set on spy via baz");
Assertions.assertEquals("name set on spy via baz", spy.getName());
spy.foo("name set on spy via foo");
Assertions.assertEquals("name set on spy via foo", spy.getName());
}
The only difference is that the Class<T>
overload of Mockito.spy
is used instead of the object spy method T
of Mockito.spy
. All assertions succeed in this case.
The same behavior can be observed with Mockito v3.3.3 and v4.7.0 (latest version of Mockito at the time of writing this question).
This behavior is documented in the JavaDoc of Mockito#spy
:
Mockito does not delegate calls to the passed real instance, instead it actually creates a copy of it. So if you keep the real instance and interact with it, don't expect the spied to be aware of those interaction and their effect on real instance state. The corollary is that when an unstubbed method is called on the spy but not on the real instance, you won't see any effects on the real instance.
And since all non-static classes automatically keep a reference to the containing instance (and that includes anonymous implementations), method calls will be dispatched to your original instance.
Crude ASCII diagram:
Spy -> original#impl -> original
Since the spy is a copy of the original, it has the same instance of the inner class. But this instance was created within the original, hence keeps a reference to the containing class (which is the original one).
The same would also be happening if you moved new SomeClass
into the constructor or field initializer. It only works there, because the call is made after the copy has been created.
If you have a debugger, you can quickly verify by setting a breakpoint after your spy was created and then compare the object ids of the impl
field. Or you make it accessible and assert:
class SpyVsSpy {
@Test
void testObjInstantiation() {
final var toBeSpied = new ClassUnderTest();
final var spy = Mockito.spy(toBeSpied);
Assertions.assertSame(toBeSpied.impl, spy.impl);
Assertions.assertNotSame(toBeSpied, spy);
}
}
class ClassUnderTest {
private String name = "initial value";
public final SomeInterface impl = new SomeInterface() {
@Override
public void foo(String name) {
}
};
private interface SomeInterface { void foo(String name);}
}
How to break #bar
:
class ClassUnderTest {
private SomeClass someClass = new SomeClass(); // keeps reference to "this"
public void baz(String name) {
someClass.foo(name);
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With