问题 我在单元测试中做了一些根本错误的事情吗?


在阅读了一篇关于单元测试行为而不是状态的有趣文章后,我开始意识到我的单元测试通常与我的代码紧密耦合,因为我正在使用模拟。 我无法在没有模拟的情况下编写单元测试图像,但事实是这些模拟将我的单元测试与我的代码非常相似,因为期望和返回调用。

例如,当我创建一个使用模拟的测试时,我会记录对特​​定模拟的所有调用并分配返回值。 现在,当我因任何原因改变实际代码的实现时,很多测试都会中断,因为模拟没有预期调用,也迫使我更新单元测试,并且有效地强迫我实现每次更改两次...... 这种情况发生了很多。

这个问题是否是使用模拟所固有的问题,我应该学会忍受它,还是我做了一些根本错误的事情? 请赐教:) 当然,欢迎随附解释的明显例子。


1176
2017-08-30 13:28


起源

你应该肯定谷歌“古典主义的模仿家福勒”,保留一两天阅读:)。 imho,两者都有其优点,两者都应该在情况需要时使用。 - Lieven Keersmaekers


答案:


当我创建一个使用模拟的测试时,   我记录了所有特定的电话   模拟并分配返回值

听起来你可能过度指定期望。

尝试在测试中尽可能少地设置代码:存根(而不是期望)与当前测试无关的所有行为,并且仅指定使测试工作绝对需要的返回值。

这个答案 包括一个简明的例子(以及另一个更详细的解释)。


5
2017-08-30 14:08





我的经验是只在(子)系统的bounderies上使用模拟。如果我有两个强相关的类,我不会嘲笑他们appart但是一起测试它们。一个例子可能是复合和访客。如果我测试一个具体的访问者,我不会使用模拟复合而是创建真正的复合材料。有人可能会说这不是单元测试(取决于什么是单位的定义)。但那并不重要。我试图实现的是:

  1. 写可读测试(没有模拟的测试大多数时候更容易阅读)。
  2. 仅测试焦点的代码区域(在示例中为concreate visitor和组合的相关部分)。
  3. 编写快速测试(只要我只实例化几个类,在示例中具体复合,这不是一个问题......注意传递创作)。

只有当我遇到子系统的边界时,我才会使用模拟器。示例:我有一个可以将自身渲染到渲染器的复合,如果我测试复合的渲染逻辑,我会模拟渲染器。

测试行为而不是状态看起来一开始就提出,但总的来说,我会测试状态,因为最终的测试很容易维护。模拟是一门大炮。不要用大锤敲打坚果。


4
2017-08-30 14:07



虽然我同意这种感觉,但很多系统在足够的地方足够浅,以至于在所有时间模拟和仅在子系统边界上进行模拟之间几乎没有区别。 - Mike Burton
在这种情况下:给系统一些深度。清楚地分离子系统! - Arne Deutsch
如果您觉得将一些课程作为一个单元一起测试很好,那么下一步可能就是将该单元合并为一个单元。 - Ladlestein
也许......但也许将所有功能打包成一个类使得这个类做得更多。 - Arne Deutsch


如果由于它们中断而修复测试,则不按预期使用它们。

如果方法的行为发生更改,则在测试驱动开发中,您首先要更改测试以期望新行为,然后实现新行为。


2
2017-08-30 13:37



这是正确的,并且是TDD的一个非常简洁的定义,但它并没有改变你总是必须实施两次更改的事实,首先在测试中,看到它失败,然后在代码中,看到它成功。那么TDD固有的“总是实现两次问题”吗? - nkr1pt
@ nkr1pt:是的,但它不是与TDD隔离的。在任何包含任何形式的测试的开发模型中,您或多或少都会遇到相同的问题。这只是在TDD中如此明显。 - Guffa


这里有几个很好的答案,但对我来说,一个好的经验法则是测试 要求 方法,而不是实现。有时这可能意味着使用模拟对象,因为交互是需求,但通常最好不要测试方法的返回值或对象状态的变化。


0
2017-08-30 19:37