使用Mockito模拟类的成员变量


136

我是开发的新手,尤其是单元测试的新手。我想我的要求很简单,但是我很想知道其他人对此的想法。

假设我有两个这样的类-

public class First {

    Second second ;

    public First(){
        second = new Second();
    }

    public String doSecond(){
        return second.doSecond();
    }
}

class Second {

    public String doSecond(){
        return "Do Something";
    }
}

假设我正在编写单元测试到测试First.doSecond()方法。但是,假设我想像这样模拟Second.doSecond()课程。我正在使用Mockito执行此操作。

public void testFirst(){
    Second sec = mock(Second.class);
    when(sec.doSecond()).thenReturn("Stubbed Second");

    First first = new First();
    assertEquals("Stubbed Second", first.doSecond());
}

我看到模拟未生效,并且断言失败。没有办法模拟我要测试的类的成员变量。?

Answers:


86

您需要提供一种访问成员变量的方式,以便您可以进行模拟传递(最常见的方式是使用参数的setter方法或构造函数)。

如果您的代码没有提供执行此操作的方法,则TDD(测试驱动开发)会将其错误地考虑在内。


4
谢谢。我看到了。我只是想知道,然后如何使用模拟执行集成测试,那里可能有许多内部方法,可能需要模拟的类,但不一定可以通过setXXX()进行设置。
Anand Hemmige,2012年

2
使用具有测试配置的依赖项注入框架。绘制您要进行的集成测试的序列图。将序列图分解为可以实际控制的对象。这意味着,如果您正在使用具有上面显示的从属对象反模式的框架类,那么就序列图而言,应将对象及其分解不良的成员视为单个单元。准备调整您控制的任何代码的因式分解,使其更具可测试性。
kittylyst,2012年

9
亲爱的@kittylyst,是的,从TDD的角度或任何理性的角度来看,这可能是错误的。但是有时开发人员会在根本没有任何意义的地方工作,而唯一的目标只是完成您分配的故事并消失。是的,这是错误的,没有道理,没有资格的人会做出关键决定和所有这些事情。因此,最终,反模式大获全胜。
amanas 2015年

1
我对此感到很好奇,如果一个类成员没有理由从外部进行设置,为什么我们仅出于测试目的而创建一个设置器?假设这里的“第二”类实际上是一个文件系统管理器或工具,在构造要测试的对象期间进行了初始化。我有充分的理由要模拟此FileSystem管理器,以便测试First类,并且有零个理由使其可以访问。我可以用Python做到这一点,那么为什么不使用Mockito呢?
Zangdar

65

如果您无法更改代码,则不可能这样做。但是我喜欢依赖注入,Mockito支持它:

public class First {    
    @Resource
    Second second;

    public First() {
        second = new Second();
    }

    public String doSecond() {
        return second.doSecond();
    }
}

您的测试:

@RunWith(MockitoJUnitRunner.class)
public class YourTest {
   @Mock
   Second second;

   @InjectMocks
   First first = new First();

   public void testFirst(){
      when(second.doSecond()).thenReturn("Stubbed Second");
      assertEquals("Stubbed Second", first.doSecond());
   }
}

这是非常容易的。


2
我认为这是一个比其他方法更好的答案,因为InjectMocks。
sudocoder

作为一个像我这样的测试新手,人们如何信任某些库和框架,这很有趣。我是假设这只是表明需要重新设计一个糟糕的主意......直到你告诉我这确实有可能在的Mockito(很清楚,干净)。
麦克啮齿动物,

9
什么是@Resource
IgorGanapolsky

3
@IgorGanapolsky @ Resource是Java Spring框架创建/使用的注释。它是一种向Spring指示这是Spring管理的bean /对象的方法。stackoverflow.com/questions/4093504/resource-vs-autowired baeldung.com/spring-annotations-resource-inject-autowire 这不是模仿对象,但是因为它是在非测试类中使用的,因此必须在测试。
Grez.Kev

我不明白这个答案。您说不可能,然后表明可能吗?这到底是不可能的?
金牌

35

如果仔细查看代码,您会发现second测试中的属性仍然是的实例Second,而不是模拟(您不会将模拟传递给first代码中)。

最简单的方法是创建一个二传手secondFirst类和显式传递的模拟。

像这样:

public class First {

Second second ;

public First(){
    second = new Second();
}

public String doSecond(){
    return second.doSecond();
}

    public void setSecond(Second second) {
    this.second = second;
    }


}

class Second {

public String doSecond(){
    return "Do Something";
}
}

....

public void testFirst(){
Second sec = mock(Second.class);
when(sec.doSecond()).thenReturn("Stubbed Second");


First first = new First();
first.setSecond(sec)
assertEquals("Stubbed Second", first.doSecond());
}

另一个是将Second实例传递为First的构造函数参数。

如果您不能修改代码,我认为唯一的选择就是使用反射:

public void testFirst(){
    Second sec = mock(Second.class);
    when(sec.doSecond()).thenReturn("Stubbed Second");


    First first = new First();
    Field privateField = PrivateObject.class.
        getDeclaredField("second");

    privateField.setAccessible(true);

    privateField.set(first, sec);

    assertEquals("Stubbed Second", first.doSecond());
}

但是您可能可以,因为很少对不受您控制的代码进行测试(尽管可以想象一种情况,您必须测试外部库,因为它的作者没有:))


得到它了。我可能会同意你的第一个建议。
Anand Hemmige,2012年

只是好奇,您是否知道有任何方法或API可以在应用程序级别或程序包级别模拟对象/方法。?我想我想说的是,在上面的示例中,当我模拟“ Second”对象时,有没有一种方法可以覆盖在测试生命周期中使用的Second的每个实例。?
Anand Hemmige 2012年

@AnandHemmige实际上第二个(构造函数)更干净,因为它避免了创建不必要的“ Second”实例。这样,您的班级就可以很好地解耦了。
soulcheck 2012年

10
Mockito提供了一些不错的注释,可让您将模拟注入到私有变量中。在初始值设定项中@Mock以注释第二和以注释第一@InjectMocks。Mockito会自动最好地找到一个位置,将Second模拟注入到First实例中,包括设置匹配类型的私有字段。
jhericks 2012年

@Mock大约是1.5(可能更早,我不确定)。1.8.3 @InjectMocks以及@Spy和介绍@Captor
jhericks 2012年

7

如果您不能更改成员变量,那么另一种方法是使用powerMockit并调用

Second second = mock(Second.class)
when(second.doSecond()).thenReturn("Stubbed Second");
whenNew(Second.class).withAnyArguments.thenReturn(second);

现在的问题是,对new Second的任何调用都将返回相同的模拟实例。但是在您的简单情况下,这将起作用。


6

我有一个相同的问题,即未设置私有值,因为Mockito不会调用超级构造函数。这是我通过反射增强模拟的方式。

首先,我创建了一个TestUtils类,其中包含许多有用的工具,包括这些反射方法。每次都需要实现反射访问。我创建了这些方法来测试由于某种原因而没有模拟包的项目上的代码,并且没有邀请我包括它。

public class TestUtils {
    // get a static class value
    public static Object reflectValue(Class<?> classToReflect, String fieldNameValueToFetch) {
        try {
            Field reflectField  = reflectField(classToReflect, fieldNameValueToFetch);
            reflectField.setAccessible(true);
            Object reflectValue = reflectField.get(classToReflect);
            return reflectValue;
        } catch (Exception e) {
            fail("Failed to reflect "+fieldNameValueToFetch);
        }
        return null;
    }
    // get an instance value
    public static Object reflectValue(Object objToReflect, String fieldNameValueToFetch) {
        try {
            Field reflectField  = reflectField(objToReflect.getClass(), fieldNameValueToFetch);
            Object reflectValue = reflectField.get(objToReflect);
            return reflectValue;
        } catch (Exception e) {
            fail("Failed to reflect "+fieldNameValueToFetch);
        }
        return null;
    }
    // find a field in the class tree
    public static Field reflectField(Class<?> classToReflect, String fieldNameValueToFetch) {
        try {
            Field reflectField = null;
            Class<?> classForReflect = classToReflect;
            do {
                try {
                    reflectField = classForReflect.getDeclaredField(fieldNameValueToFetch);
                } catch (NoSuchFieldException e) {
                    classForReflect = classForReflect.getSuperclass();
                }
            } while (reflectField==null || classForReflect==null);
            reflectField.setAccessible(true);
            return reflectField;
        } catch (Exception e) {
            fail("Failed to reflect "+fieldNameValueToFetch +" from "+ classToReflect);
        }
        return null;
    }
    // set a value with no setter
    public static void refectSetValue(Object objToReflect, String fieldNameToSet, Object valueToSet) {
        try {
            Field reflectField  = reflectField(objToReflect.getClass(), fieldNameToSet);
            reflectField.set(objToReflect, valueToSet);
        } catch (Exception e) {
            fail("Failed to reflectively set "+ fieldNameToSet +"="+ valueToSet);
        }
    }

}

然后,我可以使用这样的私有变量测试类。这对于在您也无法控制的类树中进行深度模拟非常有用。

@Test
public void testWithRectiveMock() throws Exception {
    // mock the base class using Mockito
    ClassToMock mock = Mockito.mock(ClassToMock.class);
    TestUtils.refectSetValue(mock, "privateVariable", "newValue");
    // and this does not prevent normal mocking
    Mockito.when(mock.somthingElse()).thenReturn("anotherThing");
    // ... then do your asserts
}

我在页面的实际项目中修改了代码。可能存在一两个编译问题。我认为您已大致了解。如果发现有用,请随时获取并使用它。


您可以用实际用例解释您的代码吗?像Public class tobeMocker(){private ClassObject classObject; }其中classObject等于要替换的对象。
贾斯珀·兰霍斯特

在您的示例中,如果ToBeMocker实例= new ToBeMocker(); 和ClassObject someNewInstance = new ClassObject(){@Override //类似外部依赖项};然后是TestUtils.refelctSetValue(instance,“ classObject”,someNewInstance); 请注意,您必须找出要覆盖的内容以进行模拟。假设您有一个数据库,并且此覆盖将返回一个值,因此您无需选择。最近,我有一条服务总线,我不想实际处理该消息,但想确保它收到了该消息。因此,我以这种方式设置私有总线实例-有用吗?
戴夫

您将不得不想象该注释中存在格式。它已被删除。此外,这将不适用于Java 9,因为它将锁定私有访问。正式发布后,我们将不得不使用其他一些构造,并且可以使用其实际范围。
戴夫

1

许多其他人已经建议您重新考虑您的代码以使其更具可测试性-好的建议并且通常比我要建议的更简单。

如果您无法更改代码以使其更具可测试性,请访问PowerMock:https//code.google.com/p/powermock/

PowerMock扩展了Mockito(因此您不必学习新的模拟框架),并提供其他功能。这包括使构造函数返回模拟的能力。功能强大,但有点复杂-因此请谨慎使用。

您使用其他的模拟赛跑者。并且您需要准备要调用构造函数的类。(请注意,这是一个常见的陷阱-准备调用构造函数的类,而不是构造的类)

@RunWith(PowerMockRunner.class)
@PrepareForTest({First.class})

然后在测试设置中,可以使用whenNew方法使构造函数返回一个模拟

whenNew(Second.class).withAnyArguments().thenReturn(mock(Second.class));

0

是的,可以做到,如以下测试所示(使用我开发的JMockit模拟API编写):

@Test
public void testFirst(@Mocked final Second sec) {
    new NonStrictExpectations() {{ sec.doSecond(); result = "Stubbed Second"; }};

    First first = new First();
    assertEquals("Stubbed Second", first.doSecond());
}

但是,对于Mockito,无法编写这样的测试。这是由于在Mockito中实现了模拟的方式,其中创建了要模拟的类的子类。仅此“ mock”子类的实例可以具有模拟行为,因此您需要让经过测试的代码使用它们,而不是任何其他实例。


3
问题不是JMockit是否比Mockito更好,而是如何在Mockito中做到这一点。坚持打造更好的产品,而不是寻找机会破坏竞争!
TheZuck

8
原始海报只说他正在使用Mockito。仅暗示Mockito是固定的硬性要求,因此暗示JMockit可以处理这种情况并不是那么不合适。
庞贝2014年

0

如果您想在Mockito中替代Spring的ReflectionTestUtils,请使用

Whitebox.setInternalState(first, "second", sec);

欢迎使用Stack Overflow!还提供了OP的问题的其他答案,并且这些答案已在多年前发布。发布答案时,请确保添加新的解决方案或明显更好的解释,尤其是在回答较旧的问题或对其他答案发表评论时。
help-info.de
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.