Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mockito "unwraps" spied object when using an anonymous nested interface implementation

Tags:

java

mockito

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).

  • Is this the expected behavior and if yes, what is the reason for this?
  • Is there some documentation of this behavior?
  • How can you avoid this behavior if a spy needs to be used (i.e. because of legacy tests) and no default constructor is available?
like image 411
lionheart98 Avatar asked Oct 15 '25 23:10

lionheart98


1 Answers

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);
  }
}
like image 86
knittl Avatar answered Oct 18 '25 14:10

knittl



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!