- 编写一个单元测试,以确保它向协作对象发送正确的调用和参数
- 编写一个单元测试,以确保它处理来自协作对象的所有可能响应。这些响应都是模拟的,因此该单元在隔离状态下进行测试。
- 编写一个协作对象测试,以确保它接受调用和参数。
- 编写测试以确保发送回每个可能的响应。
foo()
的响应。我将所有模拟foo()
的协作测试收集到列表中。我收集方法foo()
的所有契约测试,如果没有契约测试,则收集foo()
的所有当前实现的所有测试,并将它们放在列表中。@Ignore
(JUnit语言)或其他方式禁用模拟foo()
的协作测试,并逐个重新实现和运行这些测试,直到全部通过。我可以在不触及生产环境下的任何foo()
实现的情况下完成此操作。foo()
的对象,并使用与存根的新返回值匹配的预期结果进行实现。请记住:在协作测试中的存根对应于契约测试中的预期结果。foo()
的新响应,并且契约测试/实现测试现在期望来自foo()
的新响应,所以这应该是有效的(TM)。修改后:
这是一种权衡。通过将对象与其环境隔离来轻松测试与当所有部件组合在一起时确保其全部正常运作之间的平衡。
首先,使用集成测试来达到这种覆盖率肯定更难,所以我认为单元测试仍然更优秀。不过,我认为你说得有道理。很难保持对象的行为同步。
解决这个问题的方法是进行部分集成测试,其中包含真实服务的一级深度,但超出此范围的部分则使用模拟。例如:
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和其他语言使用异常来传达失败情况。但是,这些失败情况通常根本不是“异常”情况,这最终导致您正在处理的情况。
我认为函数式语言最接近提供最佳答案的语言。坦白地说,我认为在许多语言中都没有完美的答案,甚至没有好的答案。但是,编译时检查可以大有作为。
关于保持模拟和真实软件组件同步的问题,您甚至不能相信人类(包括自己)。
我听到你在问什么?
那么你的建议是什么?
我的建议是:
你应该编写模拟对象。
你只应该为你维护的软件组件编写模拟对象。
如果你和另一个开发者一起维护一个软件组件,你们应该一起维护该组件的模拟对象。
你不应该模拟别人的组件。
当你为你的组件编写单元测试时,你应该为该组件的模拟对象编写一个单独的单元测试。我们称之为MockSynchTest
。
在MockSynchTest
中,你应该将模拟对象的每个行为与真实组件进行比较。
当你对你的组件进行更改时,你应该运行MockSynchTest
,以查看你是否使你的模拟对象和组件失步。
如果你需要在测试你的组件时使用你不维护的组件的模拟对象,请向该组件的开发者询问模拟对象。如果她能够为你提供经过充分测试的模拟对象,那么她很棒,你也很幸运。如果她不能,那么请友好地要求她遵循这个指南并为你提供一个经过充分测试的模拟对象。
这样做,如果您意外使您的模拟不同步,那么会有一个失败的测试用例来警告您。
这样做,您无需了解外部组件的实现细节即可进行模拟。