【问题标题】:TDD how to handle a change in a mocked objectTDD 如何处理模拟对象中的更改
【发布时间】:2012-03-05 21:57:47
【问题描述】:

在编写单元测试时,对于与单元交互的每个对象,我都采取了以下步骤(从我对 JBrains 的Integration Tests are a Scam 的理解中窃取):

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

当我决定重构一个在步骤 2 中模拟了响应的对象时,我的问题就出现了。如果我更改对象响应调用的方式,那么其他对象对该调用的所有测试都不会失败,因为它们有都被嘲笑以匹配旧风格。您如何使模拟与他们正在模拟的对象保持同步?对此有最佳做法吗?还是我完全误解了一些事情并且做错了?

【问题讨论】:

    标签: unit-testing tdd


    【解决方案1】:

    我是这样做的。

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

    现在我创建一个版本控制分支,因为它会乱一会。

    @Ignore(JUnit 发言)或以其他方式禁用存根 foo() 的协作测试并开始一一重新实现和重新运行它们。我让他们都过去了。我可以做到这一点,而无需触及 foo() 的任何生产实现。

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

    此时,所有协作测试现在都假定来自foo() 的新响应,而合同测试/实施测试现在期待来自foo() 的新响应,所以它应该都可以正常工作。(TM)

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

    【讨论】:

      【解决方案2】:

      修订: 这是一个权衡。通过将对象与环境隔离开来轻松测试 vs 确信所有部分组合在一起时一切正常。

      1. 以稳定的角色为目标:考虑面向客户的角色(而不是一堆方法)。我发现角色(根据客户的需求/客户至上/由外而内编写)的波动性较小。检查角色是否是泄露实现细节的泄漏抽象。还要注意具有变革吸引力的角色(并制定缓解计划)。
      2. 如果您必须进行更改,请查看是否可以“依靠编译器”。诸如更改方法签名之类的事情将被编译器很好地标记出来。所以用它吧。
      3. 如果编译器无法帮助您发现更改,请比平时更加​​努力,看看您是否没有遗漏任何地方(客户端使用情况)。
      4. 最后,您依靠验收测试来发现此类问题 - 确保对象 A 和协作者 B、C、D 使用相同的假设(协议)。如果某样东西设法逃脱了您的拉网,那么很有可能至少有一个测试可以发现它。

      【讨论】:

      • 回归验收测试应该会有所帮助,但不是必须的。我不想尝试依赖它们,但我很高兴将它们用作紧急错误检测系统。
      • @J.B.Rainsberger - 是的。我并不是说每次交互都需要进行高级/系统测试。然而,一些系统测试应该能够发现交互中的任何异常变化。我们之前在这个tech.groups.yahoo.com/group/testdrivendevelopment/message/32743 上争吵过,不知道我是怎么错过那个线程上的最后一条消息的。我会考虑将异常抛出作为合同的一部分(您似乎暗示它们是实现细节)..您能详细说明一下吗?除此之外,只要付出足够的努力不会错过任何地方,就可以安全地进行更改
      • 首先,抛出的异常肯定是合同的一部分,但我不喜欢在合同中添加例外,除非它是必要的。接下来,最重要的是,我不喜欢您的回答将验收测试作为解决此问题的第一个也是最重要的方法,因为我通常将它们视为最后也是最薄弱的防线。我们同意,勤奋为我们提供了最大的成功机会。
      • @J.B.Rainsberger - 是的.. 我可以看到读者如何理解这个想法......已修改
      【解决方案3】:

      首先,通过集成测试获得这种覆盖率肯定更难,所以我认为单元测试仍然更胜一筹。不过,我觉得你说的有道理。很难让对象的行为保持同步。

      对此的答案是进行部分集成测试,这些测试具有 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 和其他语言使用异常来传达故障情况。但是这些故障情况通常根本不是“异常”情况,最终导致您正在处理的情况。

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

      【讨论】:

      • 我不同意保持模拟和实现同步很难。这可能很乏味,但总的来说并不难。我以 12 年前对待 TDD 的方式对待这件事:这是我工作中新发现的一部分,人们以前从未向我解释过。
      • @J.B.Rainsberger 如果您与截止日期或不喜欢单元测试的开发人员一起工作,那么保持模拟和实现真的很难。
      • @KeremBaydoğan 我在截止日期前工作。了解我编写的代码有助于我按时完成任务。让代码随着时间的推移而恶化会威胁到我按时完成任务的能力。
      • @KeremBaydoğan 如果您与不喜欢单元测试的程序员一起工作,那么您使用的每一种自动化测试技术最终都会失败。如果您想使用自动化测试来帮助您的工作,那么您会发现很难与不想以这种方式工作的人共享代码。
      • @J.B.Rainsberger 对于理解我正在处理的代码,我的感觉和你一样。我想说的是,我们不应该相信开发人员会保持模拟和真实的软件组件同步。每个开发人员都应该为他们正在维护的每个真实对象编写一个模拟对象,并且当他们完成此操作后,他们应该编写一个单元测试并将真实对象的每个行为与模拟对象的每个行为进行比较。因为评论区不够大,我贴了一个答案。如果您查看一下并判断是否有问题,那就太好了。
      【解决方案4】:

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

      我听到你问了?

      那你的建议是什么?

      我的建议是;

      1. 你应该写模拟。

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

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

      4. 你不应该模拟别人的组件

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

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

      7. 当您对组件进行更改时,您应该运行 MockSynchTest 以查看您的模拟和组件是否不同步。

      8. 如果您需要在测试组件时不维护的组件的模拟,请询问该组件的开发人员有关模拟的信息。如果她能给你提供一个经过良好测试的模拟,对她有好处,对你来说很幸运。如果她不能,请让她遵循本指南并为您提供经过良好测试的模拟。

      这样,如果你不小心让你的模拟不同步,那里会有一个失败的测试用例来警告你。

      这样你就不需要知道mock的外部组件的实现细节了。

      How-to-write-good-tests#dont-mock-type-you-dont-own

      【讨论】:

      • 在信任度非常低的环境中,这些规则可能会有所帮助,但我不建议将这些作为一般规则。我将这些规则解释为一种应对在我们与人共享代码的环境中工作的方式,但我们不作为一个团队工作。在我看来,这些规则只是简单地对待同事,就好像他们正在生产我们消费的产品一样。在这种情况下,我们不应该生活在同一个代码库中。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2015-10-13
      • 2020-07-19
      • 2010-12-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多