【问题标题】:How to conciliate TDD with SUT interface's contracts?如何协调 TDD 与 SUT 接口的合同?
【发布时间】:2010-08-30 20:52:17
【问题描述】:

假设我们使用 TDD 实现 Stack 类,我们需要为 Stack 类的每个功能添加一个新的测试来练习它:

[TestMethod] public void Should_Be_Empty_After_Instantiation()
[TestMethod] public void Should_Not_Be_Empty_After_Pushing_One_Item()
...

现在,另一方面,在进行单元测试时,应该关注我们的类应该提供的外部行为,因此单元测试集检查我的 Stack 接口的所有预期契约是否得到满足。

我的问题是如何调和这两个方面。

例如,假设我的Stack 在内部使用初始大小为 8 的数组,如果我的用户想要插入第 9 个项目,我会希望它增长。为了添加调整大小的功能,我希望至少有一个测试可以推动我的类代码朝那个方向发展(对吗?)。

另一方面,这将添加一个单元测试(或者这不是真正的单元测试吗?),它不执行类的实际合同(我假设用户不关心Stack 的内部实现)但它的实现。

所以我们这里有一个转折点,我不知道如何解决。我是否在这里混淆了概念?

谢谢

编辑

经过大量谷歌搜索后,我找到了似乎可以解决此问题的以下链接: http://stephenwalther.com/blog/archive/2009/04/11/tdd-tests-are-not-unit-tests.aspx

【问题讨论】:

    标签: unit-testing testing tdd test-first


    【解决方案1】:

    您可以编写一个将第九个项目压入堆栈的测试。如果您没有任何调整大小的逻辑,那显然会失败。但是,将 9 硬编码到测试中似乎不是一个好主意,因为您会将 Stack 的内部实现细节合并到测试中。

    现在,编写 TDD 测试通常会告知作者其 API 中可能存在的漏洞。在这种情况下,测试希望能够指定 Stack 的初始预分配大小。然后它可以将其设置为 8 或 2 或其他任何值,然后再推送一个项目。并且,认为其他客户端也可能想要它并不是不现实的(例如,它类似于 std::vector 的保留方法)。所以,我会考虑向 Stack 添加一个构造函数参数来指定初始保留大小,默认为 8,并添加一个 Should_Not_Error_When_Pushing_More_Items_Than_Initial_Size 测试。

    【讨论】:

    • 我看到的问题是,如果以后我想为使用的堆栈使用内部实现,比如说,一棵树,那么拥有容量或预分配大小。在我看来,如果我正确理解这一点,我应该能够使我的测试适用于 Tree / Array / ArrayList 实现(因为公共接口是相同的)。按照这个想法,拥有容量或分配大小的东西比任何其他东西都更难闻(所谓的泄漏抽象en.wikipedia.org/wiki/Leaky_abstraction)。
    • 回顾一下,我并不特别喜欢第二个构造函数的建议,但在 TDDing 时我看不到任何其他可行的替代方案。
    【解决方案2】:

    当提到 TDD 和特定类的外部行为时,我会说它们是同一回事。在编写 TDD 样式代码时,您关注的是使用类的公共 api 时的行为。因此,您的前两个测试用例是正确的,因为它们测试了类上的公共内容(堆栈大小)。

    至于内部实现以及是否使用数组,测试并不关心它是如何完成的,只是功能对给定的测试起作用。您可以选择在稍后阶段以不同的方式实现它,并且您的测试将验证行为保持不变(重构后测试不会失败)

    【讨论】:

    • 所以,如果我没听错的话,在向我的堆栈添加推送/弹出功能的测试方法时,并且我使用的是内部数组,我必须同时向类添加重新分配代码(虽然没有测试会明确担心重新分配,因为它是一个实现细节)。对吗?
    • 如果添加第 9 项导致它在不应该的情况下失败,那么您可以将其视为错误。您应该编写一个暴露该错误的测试,并且当它被修复时应该通过。这也适用于未来对分配方法的任何更改。
    • 这不是我的主要观点。我关心的是如何在这里应用 TDD,因为 TDD 告诉你不应该实现任何代码,除非你先写了一个测试。但在这种情况下,首先引入关于第 9 个元素的测试将与仅测试接口堆栈行为契约的单元测试相违背。
    • 这里仍然适用。当您编写用于添加项目的测试并以 8 的限制实现它时,您可以让下一个测试突出显示它将失败,因为它仍然在首先编写测试的 8 个以上。该测试仍然测试公共 API 和行为。这仍然遵循 TDD 和 Red-Green-Refactor 方法。实际上,您应该编写足够的代码来通过测试,这实际上可能意味着第一个测试确实驱动了该方法的所有功能。您最终将对单个方法进行更多测试。
    • 我之所以说这可以写成一个错误测试而不是一个正常的测试是因为它突出了 API 内部实现的一个差距。
    【解决方案3】:

    这里有几个方面的行为:

    • 允许推送和弹出项目的堆栈
    • 一个存储元素,当添加另一个项目时,它的内部数组会增长。

    如果您在测试它们时遇到问题,您总是可以将增长内部数组的行为放到另一个类中,使其与堆栈的行为分开。这是 overdrive 中的单一责任原则!然后,您可以使用一个非常大的数组来测试您的堆栈的行为,并检查堆栈是否使用模拟在涉及增长的情况下正确委派了它的职责(如果您没有模拟框架,您可以自己滚动)。

    我发现这是一种用于计时、线程或任何其他封装内部行为的有用技术,而这些内部行为不一定能从外部看到。当然,你在某种程度上牺牲了性能。如果这很重要,那么只需从调用类的角度关注堆栈的行为,并使用系统性能和分析测试来获取其余部分。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-07-26
      • 1970-01-01
      • 1970-01-01
      • 2011-07-21
      • 1970-01-01
      相关资源
      最近更新 更多