有没有更好的方法来测试以下方法而不用模拟返回模拟?

时间:2012-02-05 17:21:17

标签: java tdd mocking mockito

假设以下设置:

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的抽象,其中Context收集图元素并计算结果,这些结果被整理并返回。 actOn实际上就是叶子上的动作。

6 个答案:

答案 0 :(得分:3)

这取决于您希望测试代码的内容和数量。当您提到标记时,我认为您在任何实际生产代码之前编写了测试合同

因此,在您的合同中,您希望在actOn方法上测试什么:

  • 如果同时提供SpecificResultContext
  • ,则会返回Entity
  • 分别在add()specificResult()
  • 上发生ContextEntity次互动
  • SpecificResultResult
  • 返回的同一个实例

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

一般来说,白盒测试脆弱通常详细且不富有表现力难以重构。但它们非常适合不应该改变很多的关键部分和新手。

在你的情况下,使用模拟返回模拟看起来像白盒测试。但是如果你想在生产代码中确保这种行为,那就再好了。 Mockito可以帮助您使用深层存根。

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

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

其他评论:

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

  • 您正在使用Mockito.mock()语法在测试中创建模拟实例,如果您有多个测试,我建议您使用带有MockitoJUnitRunner注释的@Mock,这将使您的代码整齐一些,并让读者更好地了解此特定测试中发生的情况。

  • 使用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告诉我 Context is king ,你应该考虑到这个背景下的决定。但你真的应该尽可能地坚持最佳实践。

在这里和那里有很多关于测试的阅读,Martin Fowler有关于这个问题的非常好的文章,詹姆斯卡尔编制了一份test anti-patterns的清单,还有许多关于使用嘲笑的阅读材料(例如don't mock types you don't own mojo),Nat Pryce是Growing Object Oriented Software Guided by Tests的合着者,我认为这是必读书,另外还有谷歌;)

答案 1 :(得分:1)

考虑使用 fakes 而不是模拟。目前还不清楚有问题的类是什么意思,但是如果你可以构建一个简单的内存(不是线程安全,非持久性等)两个接口的实现,你可以使用它来进行灵活的测试,而不会有脆弱性有时来自嘲笑。

答案 2 :(得分:1)

我喜欢使用以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()在正确的模拟对象上运行。虽然你的原始断言并没有让它变得非常清楚被断言的是什么。所以你实际上并不需要SpecificResult的模拟。这会让你只能进行一次when通话,这对我来说似乎是正确的。

但是,这确实看起来很可怕。 Runner是公共类,还是某个更高级别进程的隐藏实现细节?如果是后者,那么你可能想要围绕更高层次的行为编写测试;而不是探讨实施细节。

答案 3 :(得分:0)

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

答案 4 :(得分:0)

虽然上下文会提供更多信息,但我自己并未发现您的测试方法存在任何问题。模拟对象的重点是验证调用行为,而不必实例化实现。我似乎没有必要创建存根对象或使用实际的实现类。

  

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

这可能更多地是关于课堂设计而不是测试。如果这是Runner类与外部接口一起工作的方式,那么我认为测试模拟该行为没有任何问题。

答案 5 :(得分:0)

首先,因为没有人提到它,Mockito支持链接所以你可以这样做:

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

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

其次,它附带一条警告,说“除遗留代码外,不要这样做”。你是关于模拟返回模拟有点奇怪。一般来说,做白盒嘲笑是可以的,因为你真的在说,“我的班级应该与像< this>”这样的帮手合作,但在这种情况下,它会在两个不同的类中进行协作,将它们连接在一起。

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

就像我说的那样,“别告诉我关于我的购物订单的所有信息,只要告诉我,我就成功了!” Runner不应该知道你只想要那个特定的结果;它应该只返回所有出现的东西,就像亚马逊向你展示你的总数,邮资和你买的所有东西一样,即使你已经在那里购物很多并且完全了解你所得到的东西。

如果有些课程经常使用你的Runner来获得特定的结果,而其他人需要更多的反馈,那么我会用两种方法来做,也许叫做addaddWithFeedback,同样的亚马逊允许您通过不同的路线单击购物的方式。

然而,要务实。如果它的可读性与你所做的一样,并且每个人都理解它,那就用Mockito链接它们并称它为一天。如果您有需要,可以稍后更改。