【问题标题】:Mockito: Mocking "Blackbox" DependenciesMockito:模拟“黑盒”依赖项
【发布时间】:2011-12-22 19:30:36
【问题描述】:

因此,我被要求为我们的开发团队阅读有关 mocking 和 BDD 的内容,并使用 mocks 来改进我们现有的一些单元测试(作为实验)。

我最终选择使用 Mockito 的原因有很多(其中一些超出了我的控制范围),但就是因为它同时支持存根和模拟,以便在不适合模拟的情况下使用。

我整天都在学习 Mockito、mock(一般)和 BDD。现在我已准备好深入研究并开始扩充我们的单元测试。

所以我们有一个名为WebAdaptor 的类,它有一个run() 方法:

public class WebAdaptor {

    private Subscriber subscriber;

    public void run() {

        subscriber = new Subscriber();
        subscriber.init();
    }
}

请注意:我无法修改此代码(出于此问题范围之外的原因!)。因此,我确实没有能够为Subscriber 添加一个setter 方法,因此它可以被认为是我的WebAdaptor 内部的一个无法访问的“黑盒”。

我想编写一个包含Mockito 模拟的单元测试,并将该模拟用于verify,执行WebAdaptor::run() 会导致调用Subscriber::init()

这就是我到目前为止所得到的(在WebAdaptorUnitTest 内):

@Test
public void runShouldInvokeSubscriberInit() {

    // Given
    Subscriber mockSubscriber = mock(Subscriber.class);
    WebAdaptor adaptor = new WebAdaptor();

    // When
    adaptor.run();

    // Then
    verify(mockSubscriber).init();
}

当我运行这个测试时,实际的 Subscriber::init() 方法被执行(我可以从控制台输出和本地系统上生成的文件中看出),不是mockSubscriber,它不应该做(或返回)任何事情。

我已经检查并重新检查:initpublic,既不是static 也不是final,它返回void。根据文档,Mockito 模拟这个对象应该没有问题。

所以我开始思考:我是否需要明确地将mockSubscriberadaptor 关联起来?如果是这种情况,那么通常情况下,以下通常会解决它:

adaptor.setSubscriber(mockSubscriber);

但由于我无法添加任何这样的设置器(请阅读我上面的注释),我不知道如何强制这样的关联。所以,几个非常密切相关的问题:

  • 谁能确认我已正确设置测试(使用 Mockito API)?
  • 我对失踪二传手的怀疑是否正确? (我需要通过 setter 关联这些对象吗?)
  • 如果我的上述怀疑属实,并且我无法修改WebAdaptor,我是否有任何规避措施?

提前致谢!

【问题讨论】:

  • 这并不能直接回答您的问题,但 JMockIt 使这种黑盒模拟非常容易。 JMockIt 适合您吗?
  • 如何在这个类中实例化订阅者?是否可以覆盖实例化代码以返回您控制的实例?
  • run() 是唯一使用订阅服务器的方法,因此无论如何它应该是该方法内的局部变量。同样,我无法更改代码...

标签: java unit-testing mocking mockito


【解决方案1】:

您需要将模拟注入到您正在测试的类中。您不需要访问订阅者。 mockito 和其他模拟框架的帮助方式是您不需要访问正在与之交互的对象。但是,您确实需要一种将模拟对象放入您正在测试的类的方法。

public class WebAdaptor {

    public WebAdaptor(Subscriber subscriber) { /* Added a new constructor */
       this.subscriber = subscriber;
    }

    private Subscriber subscriber;

    public void run() {
        subscriber.init();
    }
}

现在您可以在模拟对象上验证您的交互,而不是在真实对象上验证。

@Test
public void runShouldInvokeSubscriberInit() {

    // Given
    Subscriber mockSubscriber = mock(Subscriber.class);
    WebAdaptor adaptor = new WebAdaptor(mockSubscriber);  // Use the new constructor

    // When
    adaptor.run();

    // Then
    verify(mockSubscriber).init();
}

如果将订阅者添加到构造函数不是正确的方法,您还可以考虑使用工厂来允许 WebAdaptor 从您控制的工厂实例化新订阅者对象。然后,您可以模拟工厂以提供模拟订阅者。

【讨论】:

  • WebAdaptor 的 sn-p 仍然具有创建新订阅者的 run() 方法。仅供参考。
  • David V - 在大声听到自己的声音(正如我写这个问题时),结合您的回答和上面的 cmets,似乎更改 WebAdaptor 代码是我唯一的选择。感谢您的回复!
  • 为了让您的 WebAdaptor 类与现有代码一起工作,您可能还需要一个调用新构造函数的无参数构造函数。然后类的现有非测试用法可以使用无参数构造函数。所以新的构造函数是public WebAdaptor(){ this( new Subscriber());}。此外,具有 Subscriber 参数的构造函数应该是包私有的。
【解决方案2】:

如果您不想更改生产代码并且仍然能够模拟 Subscriber 类的功能,您应该看看 PowerMock。它与 Mockito 一起工作得很好,并允许您模拟新对象的创建。

Subscriber mockSubscriber = mock(Subscriber.class);
whenNew(Subscriber.class).withNoArguments().thenReturn(mockSubscriber);

更多细节在documentation for the PowerMock 框架中解释。

【讨论】:

【解决方案3】:

有一种方法可以将您的模拟注入到被测类中,而无需对代码进行任何修改。这可以使用 Mockito WhiteBox 来完成。这是一个非常好的功能,可用于从测试中注入待测类的依赖项。下面是一个简单的例子,说明它是如何工作的,

@Mock
Subscriber mockSubscriber;
WebAdaptor cut = new WebAdaptor();

@Before
public void setup(){
    //sets the internal state of the field in the class under test even if it is private
    MockitoAnnotations.initMocks(this);

    //Now the whitebox functionality injects the dependent object - mockSubscriber
    //into the object which depends on it - cut
    Whitebox.setInternalState(cut, "subscriber", mockSubscriber);
}

@Test
public void runShouldInvokeSubscriberInit() {
    cut.run();
    verify(mockSubscriber).init();
}

希望这会有所帮助:-)

【讨论】:

  • @zharvey 这有帮助吗?还是我弄错了?
  • 这行不通。当 cut.run() 被调用时,将在 WebAdapter 中创建一个新的、非模拟的订阅者实例,并将替换模拟的版本。然后将调用新实例的 init 方法。
【解决方案4】:

您可以在不更改原始代码的情况下使用 PowerMock 模拟构造函数调用:

import org.mockito.Mockito;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

@RunWith(PowerMockRunner.class)
@PrepareForTest(WebAdaptor.class)
public class WebAdaptorTest {
    @Test
    public void testRunCallsSubscriberInit() {
        final Subscriber subscriber = mock(Subscriber.class);
        whenNew(Subscriber.class).withNoArguments().thenReturn(subscriber);
        new WebAdaptor().run();
        verify(subscriber).init();
    }
}

【讨论】:

    【解决方案5】:

    您不能在当前实现中使用 Mockito 模拟订阅者。

    您遇到的问题是订阅者被构造然后立即访问,Mockito 在创建之后但在调用 init 方法之前无法替换(或监视)订阅者实例。

    public void run() {
    
        subscriber = new Subscriber();
        // Mockito would need to jump in here
        subscriber.init();
    }
    

    David V 的答案通过将订阅者添加到构造函数来解决这个问题。保留隐藏订阅者构造的另一种方法是在 WebAdapter 无参数构造函数中实例化订阅者,然后在调用 run 方法之前使用反射替换该实例。

    您的 WebAdapter 应该是这样的,

    public class WebAdaptor {
    
        private Subscriber subscriber;
    
        public WebAdaptor() { 
            subscriber = new Subscriber();
        }
    
        public void run() {            
            subscriber.init();
        }
    }
    

    您可以使用 Springframework 测试模块中的 ReflectionTestUtils 将依赖项注入该私有字段。

    @Test
    public void runShouldInvokeSubscriberInit() {
    
        // Given
        Subscriber mockSubscriber = mock(Subscriber.class);
        WebAdaptor adaptor = new WebAdaptor();
        ReflectionTestUtils.setField( adaptor  "subscriber", mockSubscriber );
    
        // When
        adaptor.run(); // This will call mockSubscriber.init()
    
        // Then
        verify(mockSubscriber).init();
    }
    

    ReflectionTestUtils 实际上只是一个关于 Java 反射的包装器,同样可以手动实现(并且更冗长)而不需要 Spring 依赖。

    Mockito 的 WhiteBox(正如 Bala 建议的那样)在这里可以代替 ReflectionTestUtils,它包含在 Mockito 的内部包中,所以我回避它,YMMV。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-01-30
      • 1970-01-01
      • 2020-03-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多