【问题标题】:Mockito "unwraps" spied object when using an anonymous nested interface implementation使用匿名嵌套接口实现时,Mockito \"unwraps\" 间谍对象
【发布时间】:2022-10-12 21:39:53
【问题描述】:

在处理一些遗留测试时,我最近发现了 Mockito 及其间谍的一些意外行为。考虑以下类(特别注意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);
  }
}

进一步考虑以下测试:

@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
}

我希望所有断言都能成功。然而,最后一个失败了。原因是spy.foo 通过SomeInterface 实现(impl 成员)使用“间接”。此时,间谍对象被“展开”。从impl 调用的internalFoo不是不再调用间谍,而是调用“原始”对象。基本上它是在测试用例的toBeSpied 实例上调用的,并且不是spy 实例上。 使用嵌套类时,一切都按预期工作(参见ClassUnderTest.baz,它实例化了SomeClass 对象)。

考虑以下测试:

@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());
}

唯一的区别是使用Mockito.spyClass<T> 重载而不是Mockito.spy 的对象间谍方法T。在这种情况下,所有断言都成功。

使用 Mockito v3.3.3 和 v4.7.0(撰写此问题时 Mockito 的最新版本)可以观察到相同的行为。

  • 这是预期的行为吗?如果是,这是什么原因?
  • 是否有关于这种行为的文档?
  • 如果需要使用间谍(即由于遗留测试)并且没有可用的默认构造函数,如何避免这种行为?

【问题讨论】:

    标签: java mockito


    【解决方案1】:

    此行为记录在 Mockito#spy 的 JavaDoc 中:

    莫基托才不是将调用委托给传递的真实 例如,它实际上创建了它的副本。所以如果你保持 真实的实例并与之交互,不要指望间谍是 了解这些交互及其对真实实例状态的影响。 推论是,当一个未存根方法被调用关于间谍不在真实实例上,您不会看到对真实实例的任何影响。

    而且由于所有非静态类都会自动保留对包含实例的引用(并且包括匿名实现),因此方法调用将被分派到您的原始实例。

    粗略的 ASCII 图:

    Spy -> original#impl -> original
    

    由于 spy 是原始的副本,因此它具有相同的内部类实例。但是这个实例是在原始实例中创建的,因此保留了对包含类(即原始类)的引用。 如果您将new SomeClass 移动到构造函数或字段初始值设定项中,也会发生同样的情况。它只在那里工作,因为调用是在创建副本之后进行的。

    如果你有调试器,你可以通过在你的 spy 创建后设置断点来快速验证,然后比较 impl 字段的对象 ID。或者你让它可访问并断言:

    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);}
    }
    

    如何破解#bar

    class ClassUnderTest {
      private SomeClass someClass = new SomeClass(); // keeps reference to "this"
      public void baz(String name) {
        someClass.foo(name);
      }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-03-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-10-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多