单元测试行为,无需耦合到实现细节


16

Ian Cooper 在他的演讲TDD中,哪里都出错了,将Kent Beck的初衷推到了TDD中的单元测试(测试行为,而不是具体的类方法),并主张避免将测试与实现耦合。

对于行为,例如save X to some data source在具有一组典型的服务和存储库的系统中,我们如何通过存储库对服务级别的某些数据的保存进行单元测试,而又不将测试与实现细节耦合(例如调用特定方法) )?避免这种耦合实际上不值得付出某种努力/坏处吗?


1
如果要测试数据已保存在存储库中,则必须进行测试,然后检查存储库以查看数据是否存在,对吗?还是我错过了什么?

我的问题更多是关于避免将测试耦合到实现细节上,例如在存储库上调用特定方法,或者实际上是否应该这样做。
安迪·亨特2014年

Answers:


8

您的特定示例是一种情况,您通常必须通过检查是否调用了某种方法来进行测试,因为这saving X to data source意味着要与外部依赖项进行通信,因此您必须测试的行为是通信按预期进行

但是,这不是一件坏事。应用程序及其外部依赖项之间的边界接口不是实现细节,实际上,它们是在系统体系结构中定义的;这意味着这种边界不太可能改变(或者,如果必须改变,那将是最不频繁的改变)。因此,将测试耦合到repository接口上不会给您带来太多麻烦(如果确实如此,请考虑接口是否没有从应用程序中窃取责任)。

现在,仅考虑与UI,数据库和其他外部服务分离的应用程序的业务规则。这是您必须自由更改代码的结构和行为的情况。在这里,耦合测试和实现细节将迫使您更改比生产代码更多的测试代码,即使应用程序的整体行为没有变化。这是测试State而不是Interaction帮助我们更快进行的地方。

PS:我并不是要说通过国家或互动进行测试是否是TDD的唯一真实方法-我认为这是使用正确的工具完成正确的工作的问题。


当您提到“与外部依赖项进行通信”时,您是指外部依赖项是被测单元外部还是系统整体外部的依赖项?
安迪·亨特2014年

“外部依赖关系”是指可以视为您的应用程序插件的任何内容。对于应用程序,我指的是与任何种类的细节无关的业务规则,例如用于持久性或UI的框架。我认为鲍勃叔叔可以更好地解释它,例如在这次演讲中:youtube.com/watch?
v=WpkDN78P884

我认为这是理想的方法,就像演讲中所说的那样,以“功能”或“行为”为基础进行测试,并且对每个功能或行为(或一个或多个排列,即变化的参数)进行一次测试。但是,如果我对某项功能进行了1次“高兴”测试,则为了进行TDD,这意味着我将对该功能进行一次巨大的提交(和代码审查),这是一个坏主意。如何避免这种情况?编写该功能的一部分作为测试以及与之关联的所有代码,然后在后续提交中逐步添加该功能的其余部分?
约旦2014年

我真的很想看到与实现耦合的测试的真实示例。
PositiveGuy

7

我对该演讲的解释是:

  • 测试组件,而不是类。
  • 通过其接口端口测试组件。

谈话中没有提到,但是我认为建议的假定上下文是这样的:

  • 您正在为用户开发系统,而不是为实用程序库或框架开发系统。
  • 测试的目标是在有竞争力的预算内尽可能成功地交付。
  • 组件以一种成熟的,可能是静态类型的语言(如C#/ Java)编写。
  • 组件的数量级为10000-50000行; Maven或VS项目,OSGI插件等。
  • 组件由单个开发人员或紧密集成的团队编写。
  • 您正在遵循六角形架构之类的术语和方法
  • 组件端口是您离开本地语言及其类型系统的地方,切换到http / SQL / XML / bytes / ...
  • 用Java / C#含义包装每个端口的类型化接口,可以将实现切换为交换技术。

因此,测试组件是最大的可能范围,在此范围内仍可以合理地将某些内容称为单元测试。这与某些人(尤其是学者)使用该术语的方式完全不同。它与典型的单元测试工具教程中的示例不同。但是,它确实与硬件测试中的起源相匹配。电路板和模块均经过单元测试,而不是电线和螺钉。或者至少您不制造模拟波音来测试螺丝...

从中推论出自己的想法,

  • 每个接口都将是输入,输出或协作者(例如数据库)。
  • 测试输入接口;调用方法,声明返回值。
  • 模拟输出接口;验证给定测试用例是否调用了预期的方法。
  • 伪造合作者;提供一个简单但可行的实现

如果您正确且干净地执行此操作,则几乎不需要模拟工具;每个系统只使用几次。

数据库通常是协作者,因此它是伪造的而不是被嘲笑的。手工实施会很痛苦;幸运的是,这样的事情已经存在

基本的测试模式是执行某些操作序列(例如,保存和重新加载文档);确认它有效。这与任何其他测试方案相同;没有(有效的)实现更改很可能导致此类测试失败。

例外情况是被测试系统写入但从未读取过数据库记录;例如审核日志或类似内容。这些是输出,因此应该嘲笑。测试模式是按一定顺序进行的操作;确认使用指定的方法和参数调用了审核接口。

请注意,即使在这里,只要您使用的是诸如Simito之类的类型安全的模拟工具,重命名接口方法都不会导致测试失败。如果使用加载了测试的IDE,它将与方法重命名一起进行重构。如果您不这样做,则测试将无法编译。


您能否描述/给我一个接口端口的具体示例?
PositiveGuy

什么是输出接口的示例。您能具体说明代码吗?与输入界面相同。
PositiveGuy

接口(在Java / C#意义上)包装了一个端口,该端口可以是与外界对话的任何内容(d / b,socket,http等)。输出接口是没有方法的接口,其返回值是通过端口从外部世界传来的,只有异常或等效方法。
soru's

输入接口是相反的,协作者既是输入也是输出。
soru's

1
我认为您是在谈论与视频中描述的设计方法和术语完全不同的方法。但是,存储库(即数据库)有90%的时间是协作者,而不是输入或输出。因此,它的接口是协作接口。
soru

0

我的建议是使用基于状态的测试方法:

给予 我们测试数据库处于已知状态

使用参数X调用服务时

然后 ,通过调用只读存储库方法并检查其返回值来断言数据库已从其原始状态更改为预期状态

这样,您就无需依赖服务的任何内部算法,并且可以自由地重构其实现而无需更改测试。

这里唯一的耦合是服务方法调用和从DB读取数据所需的存储库调用,这很好。

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.