【问题标题】:Mockito - Mocking Concrete ClassesMockito - 模拟具体类
【发布时间】:2013-01-17 08:07:13
【问题描述】:

给定以下代码:

    LinkedList list = mock(LinkedList.class);
    doCallRealMethod().when(list).clear();
    list.clear();

通过执行这个测试,从 LinkedList 的第一行抛出 NullPointerException#clear:

public void clear() {
    Entry<E> e = header.next;
    while (e != header) {
        Entry<E> next = e.next;
        //Code omitted. 

但是 header 之前已经实例化过:

private transient Entry<E> header = new Entry<E>(null, null, null);

有人能解释一下在模拟创建过程中发生了什么吗?

####### 更新。 ######

阅读了所有答案,尤其是 Ajay 的答案后,我查看了 Objenesis 源代码,发现它使用反射 API 创建代理实例(通过 CGLIB),因此绕过层次结构中的所有构造函数,直到 java.lang.Object。

这是模拟问题的示例代码:

public class ReflectionConstructorTest {

    @Test
    public void testAgain() {

        try {
            //java.lang.Object default constructor
            Constructor javaLangObjectConstructor = Object.class
                    .getConstructor((Class[]) null);
            Constructor mungedConstructor = ReflectionFactory
                    .getReflectionFactory()
                    .newConstructorForSerialization(CustomClient.class, javaLangObjectConstructor);

            mungedConstructor.setAccessible(true);

            //Creates new client instance without calling its constructor
            //Thus "name" is not initialized.
            Object client = mungedConstructor.newInstance((Object[]) null);

            //this will print "CustomClient" 
            System.out.println(client.getClass());
            //this will print "CustomClient: null". name is null.
            System.out.println(client.toString());

        } catch(Exception e) {
            e.printStackTrace();
        }
    }
}


class CustomClient {
    private String name;

    CustomClient() {
        System.out.println(this.getClass().getSimpleName() + " - Constructor");
        this.name = "My Name";
    }

    @Override
    public String toString() {
        return this.getClass().getSimpleName() + ": " + name;
    }
}

【问题讨论】:

    标签: java unit-testing mocking mockito


    【解决方案1】:

    您只是要求 Mockito 清除真实的东西,底层对象仍然是 Mockito 为您创建的假货。如果您需要一个真正的 LinkedList,那么只需使用 LinkedList - 只有最狂热的 BDD 纯粹主义者会告诉您模拟周围的一切。我的意思是,你不是在嘲笑字符串吗?

    Mockito 作者自己说过,调用真实的东西应该很少使用,通常只用于测试遗留代码。

    如果您需要监视真实对象(跟踪调用),那么 Mockito 也有一个功能:

    List list = new LinkedList();
    List spy = spy(list);
    

    使用 spy,如果需要,您仍然可以存根方法。它基本上像模拟一样工作,但不是;)

    【讨论】:

    • 确实如此,但没有回答问题。事实上,这样做并不是一个好主意,但我们仍然可以弄清楚为什么会发生异常,不是吗?弗拉德给出了解释,为他 +1。
    • 我很伤心:你只是要求 Mockito 将真实的东西称为 clear,对象本身没有实例化,mockito 在它下面为你创建一个假的。我已经修改了答案以使其更清楚。下次我会尽量说得更清楚。
    • 无意冒犯 :) 只是想指出,有时,有些答案不是回答问题,而是质疑 OP 的行动方针。这些有时会因为这样做而被否决。我发现这是一个很好的做法,首先告诉 OP 为什么它不能像他所做的那样工作(从而回答这个问题),然后如果我有一个可以提供的替代方案,那么我会建议一个替代方案。编辑完成后,也为你 +1。
    • 这是一个非常有效的评论 - 我只是假设“你只是要求 Mockito 将真实的东西称为清晰”意味着该对象没有其他东西是真实的。尽管如此,一个很好的观点,这一点都不清楚:)
    • @theadam:你提到代码在方法 clear 上调用真实的东西。怎么方法是真的,对象是假的?
    【解决方案2】:

    你的推理完美无缺。
    关键问题是您没有在实际的 LinkedList 对象上进行操作。以下是幕后发生的事情:

    Mockito 的 mock() 给您的对象是 CGLIB 库中的 Enhancer 对象。

    对我来说就像java.util.LinkedList$$EnhancerByMockitoWithCGLIB$$cae81a28

    哪种行为类似于代理,尽管字段设置为默认值。 (null,0 等)

    【讨论】:

    • 你是对的,但是异常是从真实对象中抛出的。即使通过调试,我也可以达到真正的方法。我的问题是如果幕后有一个真正的“LinkedList”对象,那么它的内部状态也应该没问题。
    • 之所以能够使用 LinkedList 引用来引用模拟对象意味着模拟对象是 LinkedList 的子类。希望对您有所帮助。
    • 即使mock对象是Linkedlist的子类,要创建这个子类对象,也需要创建父类对象。 (应该调用父构造函数)
    • @AjayGeorge 即使从真实代码中抛出异常,该实例仍然是一个 Mockito 模拟,并且 Mockito 正在使用技巧(通过 Objenesis)绕过任何模拟对象的默认构造函数(按顺序能够使用私有构造函数模拟类型)。模拟也将从不初始化字段,因为它不应该这样做。您想要实现的是部分模拟,您可以通过spy 来实现。另请注意,部分模拟通常表明测试代码中存在设计缺陷。
    • 您也可以从 1.9.5 开始使用委托,这与间谍不同:或通过 AdditionalAnswers.delegatesTo()
    【解决方案3】:

    当你模拟一个类时,你使用的对象是假的,因此变量没有实例化,方法也没有按预期工作。您可以使用反射为标头设置值,但我真的不推荐这样做。正如 theadam 所说,最好的办法就是使用一个列表。

    【讨论】:

      猜你喜欢
      • 2017-04-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多