【问题标题】:Mocking member variables of a class using Mockito使用 Mockito 模拟类的成员变量
【发布时间】:2012-02-18 05:07:42
【问题描述】:

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

假设我有两个这样的课程 -

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

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

【问题讨论】:

    标签: java mocking mockito


    【解决方案1】:

    您可以使用 ReflectionTestUtils 模拟 Mockito Mock 的成员变量

    ReflectionTestUtils.setField(yourMock, "memberFieldName", value);
    

    【讨论】:

      【解决方案2】:

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

      最简单的方法是在First 类中为second 创建一个setter,并显式传递给它的mock。

      像这样:

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

      但您可能可以,因为很少对您无法控制的代码进行测试(尽管可以想象一个场景,您必须测试一个外部库,因为它的作者没有:))

      【讨论】:

      • 知道了。我可能会接受你的第一个建议。
      • 只是好奇,您是否知道有任何方法或 API 可以在应用程序级别或包级别模拟对象/方法。 ?我想我要说的是,在上面的示例中,当我模拟“第二”对象时,是否有一种方法可以覆盖在整个测试生命周期中使用的第二个实例。 ?
      • @AnandHemmige 实际上第二个(构造函数)更干净,因为它避免了创建不必要的“第二个”实例。这样你的类就可以很好地解耦了。
      • Mockito 提供了一些不错的注释,让您可以将模拟注入私有变量。使用 @Mock 注释 Second 并使用 @InjectMocks 注释 First 并在初始化程序中实例化 First。 Mockito 会自动找到一个地方将 Second mock 注入到 First 实例中,包括设置匹配类型的私有字段。
      • @Mock 在 1.5 左右(也许更早,我不确定)。 1.8.3 引入了@InjectMocks 以及@Spy@Captor
      【解决方案3】:

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

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

      【讨论】:

      • 欢迎来到 Stack Overflow!还有其他答案提供了 OP 的问题,它们是多年前发布的。发布答案时,请确保添加新的解决方案或更好的解释,尤其是在回答较早的问题或评论其他答案时。
      【解决方案4】:

      如果您无法更改代码,这是不可能的。但我喜欢依赖注入,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());
         }
      }
      

      这是非常好的和简单的。

      【讨论】:

      • 我认为这是一个比其他答案更好的答案,因为 InjectMocks。
      • 有趣的是,作为一个像我这样的测试新手,如何信任某些库和框架。我以为这只是一个坏主意,表明需要重新设计......直到你向我展示它在 Mockito 中确实可能(非常清晰和干净)。
      • 什么是@Resource
      • @IgorGanapolsky @Resource 是由 Java Spring 框架创建/使用的注解。它是一种向 Spring 指示这是 Spring 管理的 bean/对象的方法。 stackoverflow.com/questions/4093504/resource-vs-autowiredbaeldung.com/spring-annotations-resource-inject-autowire这不是mockito的东西,但是因为它用在非测试类中,所以必须在测试中mock。
      • 我不明白这个答案。你说这不可能,然后你证明这是可能的?究竟什么是不可能的?
      【解决方案5】:

      您需要提供一种访问成员变量的方法,以便您可以传入模拟(最常见的方法是设置方法或带参数的构造函数)。

      如果您的代码未提供执行此操作的方法,则它被错误地考虑为 TDD(测试驱动开发)。

      【讨论】:

      • 谢谢。我看到了。我只是想知道,在可能有许多内部方法、可能需要模拟但不一定可以通过 setXXX() 事先设置的类的情况下,如何使用 mock 执行集成测试。
      • 使用依赖注入框架,带有测试配置。画出您正在尝试制作的集成测试的序列图。将序列图分解为您可以实际控制的对象。这意味着,如果您正在使用具有上面显示的依赖对象反模式的框架类,那么您应该根据序列图将对象及其分解错误的成员视为一个单元。准备好调整您控制的任何代码的因式分解,使其更具可测试性。
      • 亲爱的@kittylyst,是的,从 TDD 的角度或任何理性的角度来看,这可能是错误的。但有时开发人员在根本没有任何意义的地方工作,唯一的目标就是完成您分配的故事并离开。是的,这是错误的,没有意义,不合格的人会做出关键决定和所有这些事情。所以,归根结底,反模式赢得了很多。
      • 我很好奇,如果一个类成员没有理由从外部设置,为什么我们要创建一个setter只是为了测试它?想象一下这里的“第二”类实际上是一个文件系统管理器或工具,在构建要测试的对象期间初始化。我有充分的理由想要模拟这个 FileSystem 管理器,以测试 First 类,并且零理由让它可访问。我可以在 Python 中做到这一点,那么为什么不使用 Mockito 呢?
      【解决方案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 instance = new ToBeMocker();和 ClassObject someNewInstance = new ClassObject() { @Override //类似于外部依赖项};然后 TestUtils.refelctSetValue(instance, "classObject", someNewInstance);请注意,您必须弄清楚要为模拟覆盖什么。假设您有一个数据库,并且此覆盖将返回一个值,因此您无需选择。最近我有一个服务总线,我不想实际处理消息,但想确保它收到它。因此,我以这种方式设置私有总线实例 - 有用吗?
      • 您将不得不想象该评论中有格式。它被删除了。此外,这不适用于 Java 9,因为它会锁定私有访问。一旦我们有正式版本并且可以在其实际范围内工作,我们将不得不使用其他一些构造。
      • 这真的很有用,与其他 cmets 说的不同:“这是不可能的”或者代码必须重构,更糟糕的是,私有属性/方法要公开。这应该是公认的响应,因为它无需任何代码重构即可解决问题。
      【解决方案7】:

      是的,这可以做到,如下面的测试所示(使用我开发的 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 中实现模拟的方式,其中创建了要模拟的类的子类;只有这个“模拟”子类的实例可以具有模拟行为,所以你需要让被测试的代码使用它们而不是任何其他实例。

      【讨论】:

      • 问题不是 JMockit 是否比 Mockito 好,而是如何在 Mockito 中做到这一点。坚持打造更好的产品,而不是寻找机会破坏竞争对手!
      • 原帖只说他用的是Mockito;它只是暗示 Mockito 是一个固定且硬性的要求,因此 JMockit 可以处理这种情况的暗示并不是那么不合适。
      【解决方案8】:

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

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

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

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

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

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

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

      【讨论】:

        【解决方案9】:

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

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

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2014-06-07
          • 2012-05-25
          • 1970-01-01
          相关资源
          最近更新 更多