使用其他类(作为成员或作为方法的参数)的类需要行为正确的实例以进行单元测试。如果您有这些类可用并且它们没有引入额外的依赖项,那么使用真实的东西而不是模拟不是更好吗?
11 回答
我说尽可能使用真正的课程。
我坚信尽可能地扩展“单元”测试的边界。在这一点上,它们并不是传统意义上的真正单元测试,而只是针对您的应用程序的自动回归套件。我仍然练习 TDD 并先编写所有测试,但我的测试比大多数人的要大一些,而且我的绿-红-绿循环需要更长的时间。但是现在我已经这样做了一段时间,我完全相信传统意义上的单元测试并不是他们所吹嘘的那样。
根据我的经验,编写一堆微小的单元测试最终会成为未来重构的障碍。如果我有一个使用 B 的类 A 并且我通过模拟 B 对其进行单元测试,那么当我决定将某些功能从 A 移动到 B 或反之亦然时,我的所有测试和模拟都必须更改。现在,如果我有测试来验证系统的端到端流程是否按预期工作,那么我的测试实际上可以帮助我确定我的重构可能导致系统外部行为发生变化的地方。
最重要的是,mock 编码了特定类的契约,并且通常最终实际上也指定了一些实现细节。如果您在整个测试套件中广泛使用模拟,您的代码库最终会产生很多额外的惯性,这将抵制任何未来的重构工作。
只要您对对象有绝对的控制权,就可以使用“真实的东西”。例如,如果您有一个只有属性和访问器的对象,您可能没问题。如果您要使用的对象中有逻辑,您可能会遇到问题。
如果针对 a 类的单元测试使用了 b 类的一个实例,并且引入到 b 的更改破坏了 b,那么针对 a 类的测试也将被破坏。这是您可能遇到问题的地方,就像使用模拟对象一样,您总是可以返回正确的值。使用“真实的东西”可以使测试复杂化并隐藏真正的问题。
模拟也可能有缺点,我认为在一些模拟和一些你必须自己寻找的真实对象之间存在平衡。
您想使用存根/模拟而不是真正的类有一个非常好的理由。即使您的单元测试的(纯单元测试)类在测试中与其他所有内容隔离。这个属性非常有用,并且保持测试隔离的好处很多:
- 测试运行得更快,因为它们不需要调用真正的类实现。如果实现是针对文件系统或关系数据库运行,那么测试将变得缓慢。缓慢的测试使开发人员不经常运行单元测试。如果你在做测试驱动开发,那么耗时的测试就是对开发人员时间的毁灭性浪费。
- 如果测试与被测类隔离,则更容易追踪问题。与系统测试相比,追踪在堆栈跟踪中不明显可见或其他不可见的令人讨厌的错误将更加困难。
- 对外部类/接口所做的更改的测试不那么脆弱,因为您纯粹是在测试正在测试的类。低脆弱性也是低耦合的标志,这是一个很好的软件工程。
- 您正在测试一个类的外部行为,而不是内部实现,这在决定代码设计时更有用。
现在,如果您想在测试中使用真正的类,那很好,但它不是单元测试。相反,您正在进行集成测试,这对于验证需求和整体完整性检查很有用。集成测试不像单元测试那样经常运行,实际上它主要在提交到最喜欢的代码存储库之前完成,但同样重要。
您唯一需要记住的是以下几点:
模拟和存根用于单元测试。
真正的类用于集成/系统测试。
如果依赖项访问外部系统(如数据库或 Web 服务),我总是使用依赖项的模拟版本。
如果不是这种情况,则取决于两个对象的复杂性。用真正的依赖关系测试被测对象本质上是使两组复杂性相乘。模拟出依赖关系让我可以隔离被测对象。如果任何一个对象都相当简单,那么组合的复杂性仍然是可行的,我不需要模拟版本。
正如其他人所说,在依赖项上定义一个接口并将其注入到被测对象中可以更容易地模拟出来。
就个人而言,我不确定是否值得使用严格的模拟并验证对依赖项的每次调用。我经常这样做,但这主要是习惯。
您可能还会发现这些相关问题很有帮助:
从我的答案中提取和扩展我如何对继承对象进行单元测试?">这里:
您应该尽可能使用真实的对象。
如果真实对象做了您不想设置的事情(例如使用套接字、串行端口、获取用户输入、检索大容量数据等),您应该只使用模拟对象。从本质上讲,模拟对象用于当使用真实对象实现和维护测试的估计工作量大于使用模拟对象实现和维护测试的估计量时。
我不赞成“依赖测试失败”的论点。如果测试因依赖类损坏而失败,则测试完全按照它应该做的那样做。这不是气味!如果依赖的接口发生变化,我想知道!
高度模拟的测试环境需要非常高的维护,尤其是在项目早期接口不断变化的时候。我总是发现尽快开始集成测试更好。
仅当它首先经过单元测试时才使用真实的东西。如果它引入了阻止这种情况的依赖项(循环依赖项,或者如果它需要首先采取某些其他措施),则使用“模拟”类(通常称为“存根”对象)。
自从我被他们咬过很多次以来,我一直对嘲笑的对象非常警惕。当您想要独立的单元测试时,它们很棒,但是它们有几个问题。主要问题是,如果 Order 类需要 OrderItem 对象的集合并且您模拟它们,则几乎不可能验证模拟的 OrderItem 类的行为是否与现实世界的示例相匹配(复制具有适当签名的方法通常不是足够)。我不止一次看到系统失败,因为模拟类与真实类不匹配,并且没有足够的集成测试来捕捉边缘情况。
我通常使用动态语言进行编程,并且我更喜欢仅仅覆盖有问题的特定方法。不幸的是,这有时在静态语言中很难做到。这种方法的缺点是您使用的是集成测试而不是单元测试,并且有时更难以追踪错误。好处是您使用的是实际编写的代码,而不是该代码的模拟版本。
如果您的“真实事物”只是 JavaBeans 之类的值对象,那很好。
对于任何更复杂的事情,我会担心,因为从模拟框架生成的模拟可以被给予关于如何使用它们的精确期望,例如调用的方法的数量、精确的序列和每次预期的参数。您的真实对象无法为您执行此操作,因此您可能会在测试中失去深度。
如果您不关心验证您的 UnitUnderTest 应该如何与 Thing 交互的期望,并且与 RealThing 的交互没有其他副作用(或者您可以模拟这些),那么我认为让您的UnitUnderTest 使用 RealThing。
然后测试涵盖更多的代码库是一个奖励。
我通常发现很容易判断何时应该使用 ThingMock 而不是 RealThing:
- 当我想验证与事物交互的期望时。
- 使用 RealThing 时会带来不必要的副作用。
- 或者当 RealThing 太难/太麻烦而无法在测试环境中使用时。
如果您根据接口编写代码,那么单元测试就会变得很有趣,因为您可以简单地将任何类的假版本注入您正在测试的类中。
例如,如果您的数据库服务器由于某种原因而关闭,您仍然可以通过编写一个虚假的数据访问类来进行单元测试,该类包含一些存储在内存中的哈希映射或其他东西中的熟数据。
这取决于你的编码风格、你在做什么、你的经验和其他事情。
鉴于这一切,没有什么能阻止你同时使用.
我知道我经常使用术语单元测试方式。我所做的大部分工作可能更适合称为集成测试,但更好的是仅将其视为测试。
所以我建议使用所有适合的测试技术。总体目标是测试好,花很少的时间去做,并且个人觉得它是正确的。
话虽如此,根据您的编程方式,您可能需要考虑使用技术(如接口),使模拟的侵入性更频繁。但是不要在错误的地方使用接口和注入。此外,如果模拟需要相当复杂,则可能没有理由使用它。(你可以在这里的答案中看到很多好的指导,什么时候适合。)
换句话说: 没有答案总是有效的。保持你的智慧,观察什么有效,什么无效以及为什么。