TDD创建了一些“控制器”类 - 应该在什么样的意图上编写测试?

时间:2012-03-15 11:27:49

标签: unit-testing language-agnostic tdd

我最近开始练习TDD和单元测试,我的主要引物是优秀的GOOSGBT,并且在SO上仔细阅读TDD标记的问题。

有时,我使用的进程会创建一个“控制器”类 - 通常是一个类,它是一个相当复杂的子系统的外观,随着子系统中实现的功能数量的增加,责任不断被驱逐到辅助类中直到最初的类基本上没有责任,除了正确调用一小组协作者类并将返回的信息(如果有的话)分流到其他协作者类之外。

最初,对于即将成为控制器类的测试是在该类最终用户的意图层面上编写的:“如果我进行此调用,那么作为一个结尾应该是什么可观察的效果? - 班上的用户,真的在乎吗?“但随着越来越多的边缘案例的责任和测试被驱逐到辅助类(在控制器类的测试中被测试双打替换),这些测试开始看起来真的......模糊且非特异性:它们总是“快乐路径”测试似乎并没有真正触及问题的核心。很难解释我的意思,但是阅读测试后面给我留下了一个“那又怎样?为什么你选择这个特别的快乐路径测试而不是其他什么?有什么意义?如果有人破坏了代码,那么这个测试查明代码现在被打破的确切原因?“随着时间的推移,我越来越倾向于根据类的协作者应该如何一起使用来编写测试:“类应该在这个协作者上调用这个方法,并将结果传递给其他人合作者“这提供了一个更集中,描述性和明确动机的测试集(通常,由此产生的测试集很小)。

这显然有它的缺点:测试现在强烈地与控制器类的实现细节相关联,而不是更灵活的“这个类的最终用户看到他会关心什么?” 。但实际上,测试已经与它完全耦合了,因为它们必须配置所有Test Double协作者,使其按照实现所需的确切方式运行,以便从类的最终用户那里得到正确的结果。观点。

所以我的问题是:TDD的同事是否发现少数几个班级做的很少,但他们(小)的合作者组成一团?你是否发现保持这些类的测试从类的观点的最终用户写入是不精确和不满意的,如果是这样的话,就这些类的调用方式明确地编写这些类的测试是否可以接受在他们的合作者之间传输数据?

希望我在这里驾驶的内容相当清楚! :)


作为一个具体的例子:我正在研究的一个练习项目是电视列表下载/查看器(如果你曾经见过“Digiguide”,你会知道我的意思),我正在实施一个应用程序的核心部分 - 实际通过网络更新列表的部分,并将新下载的列表集成到当前已知的电视节目集中。这个接口(当满足所有要求时非常复杂)功能是一个名为ListingsUpdater的类,它有一个名为“updateListings”的方法。

现在,ListingsUpdater的最终用户只关心一些事情:在调用listingUpdate之后,电视列表数据库现在是否正确,是对数据库所做的更改(添加电视节目,如果广播则更改它们)发生的变化等)描述给提供的变更听众?当实施是一个非常非常简单的“伪造直到你制造它”类型的交易时,这很好用:但随着我逐步将实现推向一个可以在现实世界中运行的实现,“实际工作”得到了推动越来越远离ListingsUpdater,直到它主要编组了几个协作者:一个用于评估列表的当前状态和为ListingsDownloader构建HTTP请求的ListingsRequestPreparer,以及一个解压缩新下载的列表并将它们合并的ListingsIntegrator(它太委托了到协作者)进入列表数据库。现在,请注意,为了从用户的角度来履行ListingsUpdater的合同,我必须在测试中指示其ListingsIntegrator Test Double使用正确的数据(!)填充(假)数据库,这看起来很奇怪。放弃“从最具用户名列表中查看测试”的测试,然后添加一个测试,说明“当RichmondDownloader下载新列表时,确保将它们移交给ListingsIntegrator”。

1 个答案:

答案 0 :(得分:1)

  

这显然有它的缺点:测试现在强烈地与控制器类的实现细节相关联,而不是更灵活的“这个类的最终用户看到他会关心什么?” 。但实际上,测试已经与它完全耦合了,因为它们必须配置所有Test Double协作者,使其按照实现所需的确切方式运行,以便从类的最终用户那里得到正确的结果。观点。

我将在回复what I said时重复another question

  

我需要为每个依赖项创建一个模拟存根或一个虚拟对象[测试双]

这是常见的说法。但我认为这是错误的。如果CarEngine对象相关联,为什么在单元测试Engine课程时不使用真正的Car对象?

但是,有人会声明,如果你这样做,你就不会对你的代码进行单元测试;您的测试取决于Car Engine类:两个单元,因此是集成测试而不是单元测试。但那些人也嘲笑String课吗?还是HashSet<String>?当然不是。单元测试和集成测试之间的界限不太明确。

更哲学上,在许多情况下,你无法创建好的模拟对象[测试双打]。原因在于,对于大多数方法,对象委托给关联对象的方式是未定义的。是否委托以及如何将合同留作实施细节。唯一的要求是,在委托时,该方法满足其委托的前提条件。在这种情况下,只有一个功能齐全(非模拟)的代表才会这样做。如果真实对象检查其前提条件,则无法满足委托的前提条件将导致测试失败。调试测试失败很容易。


我将添加以回应

  

它们总是“快乐路径”测试,似乎并没有真正解决问题的核心

这是一个更一般的测试问题,不是特定于TDD或单元测试:如果您无法进行全面测试,如何选择良好的测试用例集?我依靠equivalence partitioning。当我开始处理一些代码时,我使用等价分区来选择我希望代码传递的测试用例集,然后以TDD方式依次处理每个代码,如果通过其中一个测试用例不需要更改代码(因为早期工作已经创建了满足该测试用例的代码)我仍然将测试用例添加到我的测试套件中。因此,我的测试套件有更好的coverage of potential error paths