在编写单元测试时,对于单元与之交互的每个对象,我正在采取这些步骤(从我对JBrains'Integration Tests are a Scam的理解中被盗):
当我决定重构一个在步骤2中模拟了响应的对象时,我的问题就出现了。如果我改变了对象响应调用的方式,那么其他对象对该调用的测试都不会因为它们而失败所有人都被嘲笑以配合旧式。你如何让嘲笑与他们嘲笑的对象保持同步?这是最好的做法吗?或者我是否完全误解了事情并且做错了什么?
答案 0 :(得分:3)
假设我必须更改接口方法foo()
的响应。我收集了列表中存根foo()
的所有协作测试。我收集方法foo()
的所有合同测试,或者如果我没有合同测试,我会收集列表中foo()
的所有当前实现的所有测试。
现在我创建了一个版本控制分支,因为它会在一段时间内变得混乱。
我@Ignore
(JUnit发言)或以其他方式禁用存根foo()
的协作测试,并开始逐个重新实现和重新运行它们。我让他们都过去了。我可以在不触及foo()
的任何生产实现的情况下执行此操作。
现在我重新实现逐个实现foo()
的对象,其预期结果与存根中的新返回值相匹配。请记住:协作测试中的存根对应于合同测试中的预期结果。
此时,所有协作测试现在都假定来自foo()
的新响应,并且合同测试/实现测试现在期望来自foo()
的新响应,因此它应该全部正常工作。(TM )
现在整合你的分店并给自己倒一些酒。
答案 1 :(得分:2)
修: 这是一个权衡。通过将对象与其环境隔离开来进行测试,并确保在所有部分组合在一起时可以正常运行。
答案 2 :(得分:0)
首先,通过集成测试获得此级别的覆盖率肯定更难,因此我认为单元测试仍然优越。但是,我认为你有一点意见。很难保持对象的行为同步。
对此的答案是让部分集成测试具有1级深度的实际服务,但除此之外是模拟。例如:
var sut = new SubjectUnderTest(new Service1(Mock.Of<Service1A>(), ...), ...);
这解决了保持行为同步的问题,但是复杂程度因为你现在必须设置更多的模拟。
您可以使用区分联合在函数式编程语言中解决此问题。例如:
// discriminated union
type ResponseType
| Success
| Fail of string // takes an argument of type string
// a function
let saveObject x =
if x = "" then
Fail "argument was empty"
else
// do something
Success
let result = saveObject arg
// handle response types
match result with
| Success -> printf "success"
| Fail msg -> printf "Failure: %s" msg
您定义了一个名为ResponseType
的区分联合,它具有许多可能的状态,其中一些可以带参数和其他元数据。每次访问返回值时,都必须处理可能的各种条件。如果您要添加另一个失败类型或成功类型,编译器会在您每次不处理新成员时给出警告。
这个概念在处理程序演变方面可以走很长的路。 C#,Java,Ruby和其他语言使用异常来传达故障情况。但是这些失败条件通常不是“特殊”的情况,最终导致你正在处理的情况。
我认为函数式语言最接近于为您的问题提供最佳答案。坦率地说,我认为在许多语言中都没有一个完美的答案,甚至是一个好的答案。但编译时检查可以有很长的路要走
答案 3 :(得分:0)
你不应该相信人类(甚至是你自己)关于保持模拟和真实软件组件同步。
我听你问了吗?那你的建议是什么?
我的建议是;
你应该写嘲笑。
您应该 为您维护的软件组件编写模拟。
如果您与其他开发人员维护软件组件,则您和其他开发人员应保持该组件的模拟一起。
你不应该嘲笑别人的组件。
当您为组件编写单元测试时,您应该为该组件的模拟编写单独的单元测试。我们称之为 MockSynchTest
。
在 MockSynchTest
中,您应该将模拟的每个行为与真实组件进行比较。
当您对组件进行更改时,应运行 MockSynchTest
以查看是否使模拟和组件不同步。
如果您需要模拟在测试组件时未维护的组件,请向该组件的开发人员询问有关模拟的信息。如果她可以为您提供经过良好测试的模拟,对她有好处并且对您来说很幸运。如果她不能,请让她遵循本指南并为您提供经过充分测试的模拟。
这样一来,如果你不小心让你的模拟失去同步,那里会有一个失败的测试用例来警告你。
这样您就不需要知道用于模拟的外部组件的实现细节。