有没有更好的方法可以在没有mock返回mock的情况下测试以下方法



假设以下设置:

interface Entity {}
interface Context { 
     Result add(Entity entity);
}
interface Result {
     Context newContext();
     SpecificResult specificResult();
}
class Runner {
    SpecificResult actOn(Entity entity, Context context) {
           return context.add(entity).specificResult();
    }
}

我希望看到actOn方法只是将实体添加到上下文中并返回specificResult。我现在测试的方式是以下(使用Mockito)

@Test
public void testActOn() {
    Entity entity = mock(Entity.class);
    Context context = mock(Context.class);
    Result result = mock(Result.class);
    SpecificResult specificResult = mock(SpecificResult.class);
    when(context.add(entity)).thenReturn(result);
    when(result.specificResult()).thenReturn(specificResult);
    Assert.assertTrue(new Runner().actOn(entity,context) == specificResult);
}

然而,这似乎是一个可怕的白色盒子,模拟返回模拟。我做错了什么,有人能给我指一个好的"最佳实践"文本吗?

由于人们要求更多的上下文,最初的问题是DFS的抽象,其中上下文收集图形元素并计算结果,然后进行整理并返回。actOn实际上是叶子上的动作。

这取决于您希望对代码进行测试的内容和程度。正如您提到的tdd标记一样,我想您是在任何实际生产代码之前编写测试合同的。

那么,在你的合同中,你想在actOn方法上测试什么:

  • 它在给定ContextEntity的情况下返回SpecificResult
  • add()specificResult()相互作用分别发生在ContextEntity
  • SpecificResultResult返回的实例相同
  • 等等

根据您想要测试的内容,您将编写相应的测试。如果这部分代码不是关键的,您可能需要考虑放宽测试方法。相反,如果这一部分可以触发我们所知的世界末日

一般来说,白盒测试是脆弱的通常冗长且不具表达性以及难以重构。但它们非常适合那些不应该有太大变化的关键部分和新手。

在您的案例中,有一个返回mock的mock看起来确实像是一个白盒测试。但是,如果您想确保生产代码中的这种行为,这是可以的。Mockito可以帮助你处理深桩。

Context context = mock(Context.class, RETURNS_DEEP_STUBS);
given(context.add(any(Entity.class)).specificResult()).willReturn(someSpecificResult);

但不要习惯它,因为它通常被认为是糟糕的做法和测试气味。

其他备注:

  • 您的测试方法名称不够精确,testActOn确实告诉读者您正在测试的行为。通常,tdd从业者用returns_a_SpecificResult_given_both_a_Context_and_an_Entity这样的合同语句来代替方法的名称,这显然更具可读性,并为从业者提供了测试的范围。

  • 您正在使用Mockito.mock()语法在测试中创建mock实例,如果您有几个类似的测试,我建议您使用带有@Mock注释的MockitoJUnitRunner,这将使您的代码更加整洁,并允许读者更好地了解这个特定测试中发生了什么。

  • 使用BDD(行为驱动开发)或AAA(排列行为断言)方法。

例如:

@Test public void invoke_add_then_specificResult_on_call_actOn() {
    // given
    ... prepare the stubs, the object values here
    // when
    ... call your production code
    // then
    ... assertions and verifications there
}

总而言之,正如Eric Evans告诉我的那样,上下文为王,你应该在考虑到上下文的情况下做出决定。但你们真的应该尽可能多地坚持最佳实践。

有很多关于测试的阅读,Martin Fowler在这方面有很好的文章,James Carr编制了一份测试反模式列表,也有很多关于如何使用mock的阅读(例如,你没有mojo的不要mock类型),Nat Pryce是《由测试引导的成长面向对象软件》的合著者,在我看来,这是一本必读的书,而且你有谷歌;)

考虑使用伪造而不是模拟。目前还不清楚这些类的目的是什么,但如果你能为这两个接口构建一个简单的内存中实现(不是线程安全的,不是持久性的等),你就可以使用它进行灵活的测试,而不会因为嘲笑而变得脆弱。

我喜欢为所有模拟对象使用以mock开头的名称。此外,我将取代

 when(result.specificResult()).thenReturn(specificResult); 
 Assert.assertTrue(new Runner().actOn(entity,context) == specificResult); 

带有

Runner toTest = new Runner();
toTest.actOn( mockEntity, mockContext );
verify( mockResult ).specificResult();

因为您试图断言的只是specificResult()在正确的mock对象上运行。而你最初的断言并没有把断言的内容说得那么清楚。所以实际上您不需要SpecificResult的mock。这只需要一个when电话,在我看来,这几乎适合这种测试。

但是的,这个白色盒子看起来确实很可怕。Runner是一个公共类,还是更高级别流程的一些隐藏的实现细节?如果是后者,那么你可能想围绕更高级别的行为编写测试;而不是探究实现细节。

由于不太了解代码的上下文,我建议ContextResult可能是行为很少的简单数据对象。你可以按照另一个答案中的建议使用Fake,或者,如果你可以访问这些接口的实现,并且构造很简单,我只会使用真实对象来代替Fakes或Mocks。

尽管上下文会提供更多信息,但我自己认为您的测试方法没有任何问题。mock对象的全部目的是验证调用行为,而不必实例化实现。创建存根对象或使用实际的实现类对我来说似乎没有必要

然而,这似乎是一个可怕的白色盒子,模拟返回模拟。

这可能更多的是关于类设计而不是测试。如果Runner类就是这样处理外部接口的,那么我认为让测试模拟这种行为没有任何问题。

首先,由于没有人提到它,Mockito支持链接,因此您可以执行以下操作:

when(context.add(entity).specificResult()).thenReturn(specificResult);

(请参阅Brice的评论,了解如何启用此功能;很抱歉我错过了!)

其次,它附带了一个警告:"除了遗留代码之外,不要这样做。"你说的对,mock返回mock有点奇怪。一般来说,进行白盒嘲讽是可以的,因为你真的在说,"我的类应该与一个类似<this>的助手协作",但在这种情况下,它是跨两个不同的类协作,将它们耦合在一起。

目前还不清楚为什么Runner需要获得SpecificResult,而不是context.add(entity)中的任何其他结果,所以我要做一个猜测:Result包含一个带有一些消息或其他信息的结果,你只想知道它是成功还是失败。

这就像我说,"不要告诉我所有关于我的购物订单,只要告诉我我成功了!"The Runner不应该知道你只想要那个特定的结果;它应该退回所有出来的东西,就像亚马逊向你显示你的总金额、邮费和你买的所有东西一样,即使你在那里购物过很多次,并且完全知道你得到了什么。

如果有些类经常使用Runner只是为了获得特定的结果,而其他类则需要更多的反馈,那么我会制定两种方法来实现这一点,可能被称为addaddWithFeedback,就像亚马逊让你通过不同的路线进行一键购物一样。

但是,要务实。如果它以你所做的方式可读,并且每个人都理解它,那么使用Mockito将它们链接起来,并到此为止。如果有需要,你可以稍后更改。

最新更新