为什么Mockito的"when"方法可以用于非Mock对象?

10

我最近看到一些使用 Mockito 1.9.5 的代码,它的工作方式如下:

MyObject myObject = new MyObject();
...
Mockito.when(myObject.someMethod()).thenReturn("bogus");

由于myObject并不是一个模拟对象,而是一个非模拟类的实例,我惊讶地发现这段代码编译并运行没有导致单元测试失败。我本来以为会出现失败,提示“您要求我在非模拟对象上设置期望,并且我只期望在模拟对象上设置期望。”

为什么这段代码不会导致测试失败?


更新:添加更多的代码以实际复制我觉得困惑的行为。这些示例完全说明了我的问题。下面的代码表现出了我预期的行为--当我运行这个测试时,测试失败,信息为

when()需要一个参数,它必须是'模拟上的方法调用'。

public class AnotherObject{
    public String doSomething(){
        return "did something";
    };
}

public class MyObject{
    private AnotherObject anotherObject = new AnotherObject();

    public void setAnotherObject(AnotherObject anotherObject) {
        this.anotherObject = anotherObject;
    }

    public String someMethod(){
        return anotherObject.doSomething();
    }
}

@Test
public void WhyDoesWhenWorkOnNonMock() throws Exception {
    MyObject myObject = new MyObject();
    Mockito.when(myObject.someMethod()).thenReturn("bogus");
}

如果我给这个编造的测试添加一些具体的行,尽管我之前期望的是相同的失败和相同的消息,但现在测试不再失败了:

public class AnotherObject{
    public String doSomething(){
        return "did something";
    };
}

public class MyObject{
    private AnotherObject anotherObject = new AnotherObject();

    public void setAnotherObject(AnotherObject anotherObject) {
        this.anotherObject = anotherObject;
    }

    public String someMethod(){
        return anotherObject.doSomething();
    }
}

@Test
public void WhyDoesWhenWorkOnNonMock() throws Exception {
    MyObject myObject = new MyObject();
    AnotherObject mockAnotherObject = Mockito.mock(AnotherObject.class);
    myObject.setAnotherObject(mockAnotherObject);
    Mockito.when(myObject.someMethod()).thenReturn("bogus");
}
1个回答

20

可能是由于不可思议而脆弱的巧合,除非myObject实际上被设置为间谍。

Mockito允许创建一个真实对象的"spy":

MyObject myObject = spy(new MyObject());
Mockito.when(myObject.someMethod()).thenReturn("something");

// myObject is actually a duplicate of myObject, where all the fields are copied
// and the methods overridden. By default, Mockito silently records interactions.

myObject.foo(1);
verify(myObject).foo(anyInt());

// You can stub in a similar way, though doReturn is preferred over thenReturn
// to avoid calling the actual method in question.

doReturn(42).when(myObject).bar();
assertEquals(42, myObject.bar());
除此之外,这段代码很可能不会按照预期的方式工作。 when 的参数毫无意义,只是一种语法糖,意在隐藏模拟交互是最近一次对模拟对象调用的方法。例如:
SomeObject thisIsAMock = mock(SomeObject.class);
OtherObject notAMock = new OtherObject();

thisIsAMock.methodOne();
Mockito.when(notAMock.someOtherMethod()).thenReturn("bar");
// Because notAMock isn't a mock, Mockito can't see it, so the stubbed interaction
// is the call to methodOne above. Now methodOne will try to return "bar",
// even if it isn't supposed to return a String at all!
这样的不匹配可能会导致ClassCastException、InvalidUseOfMatchersException和其他奇怪的错误。另外,你真正的MyObject类可能会将mock作为参数,最后一次交互是与someMethod交互的mock。
你的编辑证实了我的猜测。对于Java而言,在调用when之前,它需要评估when的参数,所以你的测试调用了someMethod(真实)。Mockito看不到这一点 - 它只能在你与它的mocks之一交互时才采取行动 - 所以在第一个例子中,它看不到与mocks的任何交互,从而导致失败。在你的第二个例子中,你的someMethod调用了doSomething,Mockito可以看到它并返回默认值(null),并标记它为最近的方法调用。然后发生对when(null)的调用,Mockito忽略参数(null)并引用最近调用的方法(doSomething),并从此时开始存根以返回“bogus”。
即使你从未明确地进行过存根操作,通过将以下断言添加到你的测试中,你也可以看到这一点:
assertEquals("bogus", mockAnotherObject.doSomething());

作为一个额外的参考,我写了一篇有关Mockito匹配器的单独SO答案,其中的实现细节可能会很有用。请参阅步骤5和6以获取类似问题的扩展视图。


谢谢!我可以确认myObject没有被设置为间谍,而真正的MyObject类不会将模拟作为参数。我同意代码并没有做原作者可能想要的事情。我只是惊讶它没有失败。你关于模拟交互是“最近一次对模拟的调用”的评论似乎是解释的原因,所以我添加了更新来尝试使场景完整和清晰。真正的类使用另一个对象,当该对象是模拟时,我没有得到预期的结果。 - Shawn
1
@Shawn 不用谢!我更新了我的答案,以注释你编辑的两个示例中正在发生的事情。 - Jeff Bowman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接