假设以下设置:
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实际上就是叶子上的动作。
答案 0 :(得分:3)
这取决于您希望测试代码的内容和数量。当您提到tdd标记时,我认为您在任何实际生产代码之前编写了测试合同。
因此,在您的合同中,您希望在actOn
方法上测试什么:
SpecificResult
和Context
Entity
add()
和specificResult()
Context
,Entity
次互动
SpecificResult
是Result
根据您要测试的内容,您将编写相应的测试。您可能需要考虑放宽您的测试方法,如果这段代码不至关重要。如果这部分可以触发我们所知道的世界末日,则相反。
一般来说,白盒测试脆弱,通常详细且不富有表现力,难以重构。但它们非常适合不应该改变很多的关键部分和新手。
在你的情况下,使用模拟返回模拟看起来像白盒测试。但是如果你想在生产代码中确保这种行为,那就再好了。 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()
语法在测试中创建模拟实例,如果您有多个测试,我建议您使用带有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)
我不太了解代码的上下文,我建议Context
和Result
可能是行为很少的简单数据对象。您可以使用另一个答案中建议的假,或者,如果您可以访问这些接口的实现并且构造很简单,我只需使用真实对象代替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来获得特定的结果,而其他人需要更多的反馈,那么我会用两种方法来做,也许叫做add
和addWithFeedback
,同样的亚马逊允许您通过不同的路线单击购物的方式。
然而,要务实。如果它的可读性与你所做的一样,并且每个人都理解它,那就用Mockito链接它们并称它为一天。如果您有需要,可以稍后更改。