TDD如何处理模拟对象中的更改



在编写单元测试时,对于单元与之交互的每个对象,我都会采取以下步骤(从我对JBrains的集成测试的理解中窃取):

  1. 在单元中编写一个测试,以确保它向协作对象发送正确的调用和参数
  2. 在单元中编写一个测试,确保它处理来自协作对象的所有可能的响应。这些响应都是模拟的,所以单元是孤立测试的
  3. 在协作对象中编写一个测试,以确保它接受调用和params
  4. 编写测试以确保每个可能的响应都被发回

当我决定重构一个在步骤2中模拟了响应的对象时,我的问题就来了。如果我改变对象对调用的响应方式,那么其他对象对该调用的测试都不会失败,因为它们都被嘲笑为匹配旧样式。如何使mock与它们正在嘲笑的对象保持最新?对此有最佳实践吗?还是我完全误解了事情,完全做错了?

我是这样做的。

假设我必须更改接口方法foo()的响应。我在一个列表中收集了所有截断foo()的协作测试。我收集方法foo()的所有契约测试,或者如果我没有契约测试,我将foo()的所有当前实现的所有测试收集在一个列表中。

现在我创建了一个版本控制分支,因为它在一段时间内会很混乱。

I @Ignore(JUnit speak)或以其他方式禁用截断foo()的协作测试,并开始逐一重新实现和运行它们。我让他们都通过了。我可以在不接触foo()的任何生产实现的情况下做到这一点。

现在,我一个接一个地重新实现实现foo()的对象,其预期结果与存根的新返回值相匹配。记住:协作测试中的存根对应于合同测试中的预期结果。

在这一点上,所有的协作测试现在都假设来自foo()的新响应,而合同测试/实现测试现在都期望来自foo()的新响应。(TM)

现在整合你的分支,给自己倒点酒。

修订:这是一种权衡。通过将对象与其环境隔离来测试的容易性与当所有部分结合在一起时它都能工作的信心。

  1. 目标是稳定的角色:考虑面向客户的角色(而不是一堆方法)。我发现角色(根据客户的需求/客户优先/外部-内部编写)的波动性较小。检查角色是否是泄露实现细节的泄漏抽象。还要注意那些吸引变化的角色(并制定一个缓解计划)
  2. 如果需要进行更改,请查看是否可以"依赖编译器"。像更改方法签名这样的事情会被编译器很好地标记出来。所以使用它
  3. 如果编译器不能帮助您发现更改,要比平时更加勤奋,看看您是否没有错过一个位置(客户端使用)
  4. 最后,您可以通过验收测试来发现这些问题——确保对象A和协作者B、C、D按照相同的假设(协议)进行操作。如果某个东西成功逃脱了你的罗网,那么至少有一次测试发现它的可能性很高

首先,集成测试肯定更难达到这样的覆盖率,所以我认为单元测试仍然更出色。不过,我认为你说得有道理。很难使对象的行为保持同步。

对此的一个答案是进行部分集成测试,这些测试具有1级深度的真实服务,但除此之外是模拟。例如:

var sut = new SubjectUnderTest(new Service1(Mock.Of<Service1A>(), ...), ...);

这解决了保持行为同步的问题,但增加了复杂性,因为您现在必须设置更多的mock。

您可以在函数式编程语言中使用有区别的并集来解决这个问题。例如:

// 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和其他语言使用异常来传达故障条件。但这些故障情况通常根本不是"特殊"情况,这最终会导致您正在处理的情况。

我认为函数式语言最接近于为您的问题提供最佳答案。坦率地说,我不认为有一个完美的答案,甚至在许多语言中都没有一个好的答案。但是编译时检查在很大程度上有助于

您不应该相信人类(甚至您自己)能够保持模拟和真实软件组件的同步。

我听到你问了吗?

那你的建议是什么?

我的建议是

  1. 你应该写模仿。

  2. 您应该维护的软件组件编写模拟。

  3. 如果您与其他开发人员一起维护软件组件,则您和其他开发人员应一起维护该组件的mock。

  4. 你不应该嘲笑别人的组件

  5. 当您为组件编写单元测试时,您应该为该组件的mock编写一个单独的单元测试。让我们称之为MockSynchTest

  6. MockSynchTest中,您应该将mock的每个行为与实际组件进行比较。

  7. 当您对组件进行更改时,应该运行MockSynchTest来查看是否使mock和组件不同步。

  8. 如果您需要在测试组件时没有维护的组件的mock,请询问该组件的开发人员有关mock的信息。如果她能为你提供一个测试良好的模拟,对她来说是好事,对你来说也是幸运。如果她做不到,请她遵循此指南,并为您提供一个经过充分测试的模拟。

这样,如果您不小心使mock不同步,就会有一个失败的测试用例来警告您。

这样,您就不需要知道用于mocking的外部组件的实现细节。

如何编写好的测试#不要模仿你没有的类型

最新更新