8

我仍处于关于单元测试的学习阶段,特别是关于模拟(我正在使用PascalMockDUnit框架)。我现在偶然发现的一件事是,我找不到将测试类/接口的实现细节硬编码到我的单元测试中的方法,而且感觉不对……

例如:我想测试一个类,它实现了一个非常简单的接口,用于读取和写入应用程序设置(基本上是名称/值对)。呈现给消费者的界面完全不知道值的实际存储位置和方式(例如注册表、INI 文件、XML、数据库等)。自然地,访问层是由另一个不同的类实现的,该类在构造时被注入到测试类中。我为此访问层创建了一个模拟对象,现在我能够完全测试接口实现类,而无需实际读取或写入任何注册表/INI 文件/任何内容。

但是,为了确保模拟对象在被测试类访问时的行为与真实对象完全一样,我的单元测试必须通过非常明确地定义预期的方法调用和被测类预期的返回值来设置模拟对象。这意味着如果我必须更改访问层的接口或测试类使用该层的方式,我还必须更改内部使用该接口的类的单元测试,即使接口我实际上正在测试的课程根本没有改变。这是我在使用模拟时必须忍受的事情,还是有更好的方法来设计可以避免这种情况的类依赖关系?

4

3 回答 3

7

为了确保模拟对象在被测试类访问时的行为与真实事物完全一样,我的单元测试必须通过非常明确地定义预期的方法调用和被测类预期的返回值来设置模拟对象。

正确的。

更改访问层的接口或测试类使用该层的方式我还必须更改单元测试

正确的。

即使我实际测试的类的接口根本没有改变。

“实际测试”?你的意思是暴露的接口类?没关系。

“测试”(接口)类使用访问层的方式意味着您已将内部接口更改为访问层。接口更改(甚至是内部更改)需要测试更改,如果您做错了什么,可能会导致损坏。

这没什么错。事实上,关键是对访问层的任何更改都必须要求对模拟进行更改以确保更改“有效”。

测试不应该是“健壮的”。应该是脆的 如果您做出改变内部行为的更改,那么事情可能会破裂。如果你的测试太健壮,他们就不会测试任何东西——他们只会工作。这是错误的。

测试应该只在完全正确的原因下工作。

于 2010-08-10T11:16:35.853 回答
6

这是我在使用模拟时必须忍受的事情,还是有更好的方法来设计可以避免这种情况的类依赖关系?

很多时候,模拟(特别是像 JMock 这样的敏感框架)会迫使您考虑与您尝试测试的行为没有直接关系的细节,有时这甚至可以通过暴露可疑代码来帮助您做太多并且有太多的调用/依赖。

但是在你的情况下,如果我没看错你的描述,听起来你真的没有问题。如果您正确设计了读/写层并具有适当的抽象级别,则不必更改它。

这意味着如果我必须更改访问层的接口或测试类使用该层的方式,我还必须更改内部使用该接口的类的单元测试,即使接口我实际上正在测试的课程根本没有改变。

写抽象的访问层不是为了避免这种情况吗?一般来说,遵循Open/Closed 原则,这种接口不应该改变,也不应该破坏与使用它的类的约定,并且通过扩展它也不会破坏你的单元测试。现在,如果您更改方法调用的顺序,或者必须对抽象层进行新的调用,那么,是的,特别是对于某些框架,您的模拟期望将会中断。这只是使用模拟成本的一部分,而且完全可以接受。但一般来说,界面本身应该保持稳定。

于 2010-08-10T16:55:56.967 回答
1

只是给你的例子加上一些名字,

  • RegistryBasedDictionary 实现角色(接口)字典。
  • RegistryBasedDictionary 依赖于由 RegistryWinAPIWrapper 实现的 Role RegistryAccessor。

您目前有兴趣测试 RegistryBasedDictionary。单元测试将为 RegistryAccessor 角色注入一个模拟依赖项,并测试与依赖项的预期交互。

  • 避免不必要的测试维护的技巧是“精确地指定应该发生的事情......而不是更多。 ”(来自GOOS 书(必须阅读模拟风格的 TDD),所以如果依赖方法调用的顺序无关紧要, 不要在测试中指定它。这样您就可以自由地更改实现中的调用顺序。)
  • 设计角色,使它们不包含来自实际实现的任何泄漏 -保持角色实现不可知

更改 RegistryBasedDictionary 测试的唯一原因是更改 RegistryBasedDictionary 的行为,而不是其任何依赖项。因此,如果它与其依赖项的交互或角色/合同发生变化,则需要更新测试。这就是您需要支付的基于交互的测试的价格,以实现独立测试的灵活性。然而在实践中,这种情况并不经常发生。

于 2010-08-11T10:02:14.697 回答