gpt4 book ai didi

java - 使用匿名嵌套接口(interface)实现时 Mockito "unwraps" spy 对象

转载 作者:行者123 更新时间:2023-12-05 05:35:26 25 4
gpt4 key购买 nike

在处理一些遗留测试时,我最近发现了 Mockito 及其 spy 的一些意外行为。考虑以下类(特别注意 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 成员)。此时,侦测对象被“展开”。 internalFoo这是从 impl 调用的不再被 spy 调用,而是被“原始”对象调用。基本上它是在 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());
}

唯一的区别是 Class<T>过载 Mockito.spy用于代替对象 spy 方法 TMockito.spy .在这种情况下所有断言都成功。

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

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

最佳答案

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

Mockito does not delegate calls to the passed realinstance, instead it actually creates a copy of it. So if you keepthe real instance and interact with it, don't expect the spied to beaware 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.

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

粗略的 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);
}
}

关于java - 使用匿名嵌套接口(interface)实现时 Mockito "unwraps" spy 对象,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/73517738/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com