Mockito:模拟“黑盒”依赖项

11

我被要求为我们的开发团队学习模拟和BDD并尝试使用模拟来改进我们现有的一些单元测试(作为实验)。

最终我选择了Mockito,原因有几个(其中一些超出了我能控制的范围),但主要是因为它支持存根和模拟,以处理不适合进行模拟的情况。

我花了一整天的时间学习Mockito,模拟(总体而言)和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);

但由于我无法添加任何这样的setter(请阅读我上面的注释),因此我不知道如何强制进行这样的关联。因此,有几个非常相关的问题:

  • 是否有人可以确认我是否已经正确设置了测试(使用Mockito API)?
  • 我的对于缺失setter的怀疑是否正确?(我需要通过setter关联这些对象吗?)
  • 如果我上面的怀疑是真的,并且我不能修改WebAdaptor,那么是否有任何绕过方法?

提前感谢!


这并不是直接回答你的问题,但是JMockIt使得这种黑盒模拟变得非常容易。JMockIt对你来说是一个选择吗? - Cameron Skinner
在这个类中,Subscriber是如何实例化的?是否有可能覆盖实例化代码以返回一个由您控制的实例? - Mike Yockey
run() 是唯一使用 Subscriber 的方法,因此它应该是该方法内的局部变量。再次强调,我无法更改代码... - IAmYourFaja
5个回答

10

你需要将模拟对象注入到正在测试的类中。你不需要访问Subscriber。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();
}

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


你的WebAdaptor代码片段仍然有run()方法创建新的Subscriber。供参考。 - Paul Morie
David V - 在听到自己大声朗读这个问题后,再加上您的回答和上面的评论,似乎修改“WebAdaptor”代码是我唯一的选择。感谢您的回复! - IAmYourFaja
2
为了让您的WebAdaptor类与现有代码保持兼容,您可能还需要拥有一个无参构造函数来调用您的新构造函数。这样,该类现有的非测试用途可以使用无参构造函数。因此,新的构造函数应该是public WebAdaptor(){ this( new Subscriber());}。另外,带有Subscriber参数的构造函数应该是包私有的。 - Dawood ibn Kareem

5

如果您不想更改生产代码,但仍然希望模拟Subscriber类的功能,则应该看看PowerMock。它与Mockito一起使用效果很好,并允许您模拟创建新对象。

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

更多细节在PowerMock框架的文档中有解释。


请参阅 示例#3-Mock构造新对象(向下滚动)以获取更详尽的示例。 - matsev

2

有一种方法可以在不修改代码的情况下将模拟对象注入到被测试类中。这可以使用Mockito的WhiteBox来实现。这是一个非常好的功能,可以用于从测试中注入您的Class Under Test的依赖项。以下是一个简单的示例说明其工作原理:

@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 这个有帮助吗?还是我搞错了? - Bala
2
这样做不会起作用。当调用cut.run()时,WebAdapter内部将创建一个新的非模拟的Subscriber实例,并替换模拟版本。然后将调用新实例上的init方法。 - Dan Midwood

2
您可以使用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();
    }
}

1

在您当前的实现中,无法使用Mockito模拟Subscriber。

您面临的问题是Subscriber被构建后立即访问,Mockito无法在调用init方法之前替换(或监视)Subscriber实例。

public void run() {

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

David V的答案通过将Subscriber添加到构造函数中来解决了这个问题。另一种保留隐藏的Subscriber构造的方法是在WebAdapter无参构造函数中实例化Subscriber,然后使用反射在调用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 的内部包中,所以我不太敢用,可能会有不同情况。


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