为单元测试伪造依赖项(例如Doctrine)还是使用真实的依赖项更好?
2 回答
在单元测试中,您只使用一个类的真实实例,这就是您要测试的类。
该类的所有依赖项都应该被模拟,除非有理由不这样做。
不模拟的原因是,如果正在使用本身没有依赖关系的数据对象 - 您可以使用真实对象并测试它之后是否接收到正确的数据。
另一个不模拟的原因是如果模拟的配置太复杂 - 在这种情况下,你有理由重构代码,因为如果模拟一个类太复杂,该类的 API 可能太复杂,也。
但一般的答案是:您希望每次都模拟每个依赖项。
我会给你一个“too-complicated-so-refactor”案例的例子。
我使用“Zend_Session_Namespace”对象来存储模型的内部数据。该实例被注入到模型中,因此模拟不是问题。
但是真正的“命名空间”类的内部实现让我模拟了所有对模型的调用__set
以及__get
它们在模型中的使用顺序。那糟透了。因为每次我决定重新排序代码中值的读取和写入时,我都必须更改测试中的模拟,尽管没有任何问题。代码中的重构不应导致测试中断或强迫您更改它们。
重构添加了一个新对象,将“Zend_Session_Namespace”与模型分开。我创建了一个扩展“ArrayObject”并包含“命名空间”的对象。在创建时,从命名空间读取所有值并添加到 ArrayObject,并且在每次写入时,该值也会传递给命名空间对象。
我现在的情况是,我可以在所有测试中使用真正的扩展 ArrayObject,它本身只需要一个未配置的“Zend_Session_Namespace”模拟实例,因为我不需要测试这些值是否正确存储在我的会话中测试了模型。我只需要一个在模型内部使用的数据存储。
为了测试会话是否被正确读取和写入,我对该 ArrayObject 本身进行了测试。
所以最后我使用了模型的真实实例,数据存储的真实实例以及“Zend_Session_Namespace”的模拟实例,它什么都不做。我故意选择将之前混入模型类的“模型东西”和“会话保存东西”分开->“单一责任原则”。
这样测试真的变得更容易了。而且我想说这也是一种代码异味:如果创建和配置模拟类很复杂,或者在更改测试类时需要进行大量更改,那么是时候考虑重构了。那里有问题。
嘲笑应该是有原因的。好的理由是:
- 您不能轻易地使依赖组件 (DOC) 的行为符合您的测试预期。
- 调用 DOC 是否会导致任何非确定性行为(日期/时间、随机性、网络连接)?
- 测试设置过于复杂和/或维护密集(例如,需要外部文件)
- 原始 DOC 为您的测试代码带来了可移植性问题。
- 使用原始 DOC 会导致构建/执行时间过长吗?
- 是否存在导致测试不可靠的 DOC 稳定性(成熟度)问题,或者更糟糕的是,DOC 甚至还没有可用?
例如,您(通常)不会模拟sin
or之类的标准库数学函数cos
,因为它们没有任何上述问题。
为什么建议避免在不必要的地方嘲笑?
一方面,模拟增加了测试的复杂性。
其次,模拟使您的测试依赖于代码的内部工作,即代码如何与 DOC 交互。对于测试已实现算法的白盒测试是可以接受的,但对于黑盒测试来说是不可取的。