TDD:模拟出紧密耦合的对象


10

有时只需要紧密耦合对象即可。例如,一个CsvFile类可能需要与CsvRecord该类(或ICsvRecord接口)紧密配合。

但是,从我过去的经验中学到,测试驱动开发的主要宗旨之一是“永远不要一次测试一个以上的类”。表示您应该使用ICsvRecord模拟或存根,而不是的实际实例CsvRecord

但是,在尝试这种方法之后,我注意到嘲笑CsvRecord该类可能会变得有些毛茸茸。这使我得出以下两个结论之一:

  1. 编写单元测试很困难!那是代码的味道!重构!
  2. 模拟每个依赖项是不合理的。

当我用实际CsvRecord实例替换模拟对象时,事情进行得更加顺利。当寻找其他人的想法时,我偶然发现了这篇博客文章,该文章似乎支持上面的第二点。对于自然紧密耦合的对象,我们不必太担心模拟。

我会偏离轨道吗?以上假设2有不利之处吗?我是否真的应该考虑重构设计?


1
我认为,“单元测试”中的“单元”必须是一类是一个普遍的误解。我认为您的示例显示了一种情况,那就是最好将这两个类合并为一个单元。但是请不要误会我的意思,我完全同意罗伯特·哈维的回答。
布朗

Answers:


11

如果您确实需要在这两个类之间进行协调,请编写一个CsvCoordinator封装两个类的类,然后进行测试。

但是,我对CsvRecord不可独立测试的观点表示质疑。 CsvRecord基本上是DTO类,不是吗?它只是字段的集合,可能带有一些辅助方法。并且CsvRecord可以在其他情况下使用CsvFile; 例如,您可以具有的集合或数组CsvRecord

CsvRecord首先测试。确保它通过了所有测试。然后,继续进行测试,并CsvRecord与您的CsvFile班级一起使用。将其用作预先测试的存根/模拟;用相关的测试数据填充它,并将其传递给CsvFile,然后针对该数据编写测试用例。


1
是的,CsvRecord绝对可以独立测试。问题是,如果CsvRecord中的某些内容中断,则将导致CsvData测试失败。但是我认为这不是主要问题。
Phil

1
我想你想做到这一点。:)
罗伯特·哈维

1
@RobertHarvey:从理论上讲,如果CsvRecord和CsvFile成为非常复杂的类,并且如果CsvFile的测试失败,则可能会成为问题,现在您不立即知道CsvFile或CsvRecord是否有问题。但是我想这更多是一种假设情况-如果我要为现实程序编写此类的任务,那么我将按照您描述它的方式进行。
布朗

2
@Phil:如果CsvRecord中断,那么显然会CsvData失败;但这没关系,因为您CsvRecord先进行测试,如果失败,则您的CsvFile测试毫无意义。您仍然可以区分CsvRecord和中的错误CsvFile
tdammers 2012年

5

一次测试一个类的原因是,您不希望对一个类的测试依赖于第二个类的行为。这意味着,如果对A类的测试行使了B类的任何功能,则应模拟B类以消除对B类中特定功能的依赖。

CsvRecord在我看来,一个类似乎主要是用于存储数据,而不是一个功能太多的类。也就是说,它可能具有构造函数,getter,setter,但没有真正具有实质逻辑的方法。当然,我在这里猜测-也许您编写了一个名为的类CsvRecord,它执行大量复杂的计算。

但是,如果CsvRecord没有自己真正的逻辑,那么通过嘲笑就没有任何收获。这实际上只是旧的格言- “不要嘲笑值对象”

因此,在考虑是否模拟特定的类(用于测试不同的类)时,应考虑该类具有多少自己的逻辑,以及在测试过程中将执行多少逻辑。


+1。任何结果取决于一个以上对象行为的正确性的测试都是集成测试,而不是单元测试。您必须模拟掉这些对象之一才能获得真实的单元测试。但是,这不适用于其中没有实际行为的对象-例如仅具有getter和setter的对象。
guillaume31

1

2号没问题。如果事物的概念紧密相关,那么事物可以并且应该紧密相关。这应该很少见,通常应该避免,但是在您提供的示例中这是有道理的。


0

“耦合”类相互依赖。在您所描述的情况下应该不是这种情况-CsvRecord不应真正在乎包含它的CsvFile,因此依赖关系只是一种方式。很好,并且不是紧密耦合。

毕竟,如果一个类包含变量String name,那么您不会声称它与String紧密耦合,对吗?

因此,对CsvRecord进行所需的行为的单元测试。

然后使用一个模拟框架(Mockito非常棒)来测试您的单元是否与它正确依赖的对象进行交互。实际上,您要测试的行为是CsvFile以预期的方式处理CsvRcords。CvsRecord的内部工作无关紧要-CvsFile与之通信的方式。

最后,TDD 不仅涉及单元测试。您当然可以(并且应该)从功能测试开始,这些功能测试着眼于大型组件如何工作的功能行为-即您的用户故事或场景。您的单元测试确定了期望并验证了各个部分,而功能测试对于整个过程来说都是相同的。


1
-1,紧密耦合并不一定意味着循环依赖性,这是一个误解。在示例中,CsvFile 紧密耦合CsvRecord(但并非相反)。OP询问通过将CsvFile其与CsvRecord进行解耦来进行测试是否是一个好主意ICsvRecord,反之亦然。
布朗

2
@DocBrown:耦合是否紧密CsvFile取决于大小,这取决于文件的内部工作原理CsvRecord,即文件对记录的假设量。接口可以帮助记录和执行此类假设(或者说没有其他假设),但是耦合的数量保持不变,除了使用接口,您可以将另一个记录类挂接到中CsvFile。引入接口只是为了说减少耦合是愚蠢的。
tdammers

0

这里确实有两个问题。首先是如果存在不宜嘲笑对象的情况。如其他出色的答案所示,这无疑是正确的。第二个问题是您的特殊情况是否是其中一种情况。在这个问题上,我不相信。

不模拟类的最常见原因可能是它是值类。但是,您必须查看规则背后的原因。这不是因为嘲笑的类会以某种方式变坏,而是因为它实际上与原始类相同。如果是这样的话,使用原始类将不会使您的单元测试变得更加容易。

很有可能您的代码是重构无法提供帮助的极少数例外之一,但是您应该在勤奋的重构工作没有奏效之后才声明它。即使是经验丰富的开发人员,也很难找到自己设计的替代方案。如果您想不出任何改进方法,请有经验的人再看一遍。

大多数人似乎都在假设您CsvRecord是价值类。尝试使其成为一个。如果可以的话,使其保持不变。如果您有两个互相指向指针的对象,请删除其中之一,并弄清楚如何使其工作。寻找拆分类和函数的地方。拆分类的最佳位置可能并不总是与文件的物理布局匹配。尝试反转类的父/子关系。也许您需要一个单独的类来读取和写入csv文件。也许您需要单独的类来处理文件I / O和上层接口。在声明它不可重构之前,有很多尝试。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.