【问题标题】:Reducing Coupling Between Test Cases减少测试用例之间的耦合
【发布时间】:2013-02-05 11:38:15
【问题描述】:

我正在尝试了解有关 JUnit 和 TDD 的更多信息,但我遇到了一些测试用例之间耦合的问题。

当我为特定数据类型的 API 编写测试用例时,比如Deque<T>,我如何限制测试用例之间的耦合?例如,如果我正在为方法 insertFirst(T item) 编写一个测试用例,那么在正确初始化的对象上调用该方法后,我应该能够断言两件事似乎很简单:

  1. Deque 对象的大小应该增加一
  2. 如果我随后调用相应的T removeFirst() 方法,它应该返回对我在初始调用中插入的对象的引用。

但是,这会在我的至少两个测试用例之间产生不希望的耦合,其中一个测试用例的通过取决于另一个 API 方法的正确实现。例如,为了让这个测试用例通过,我需要一个正确的实现来检查Deque 中的项目数以及删除项目。如果我对这些方法中的任何一个的测试由于某种原因不正确或不完整,那么我对insertFirst 方法的测试将自动受到怀疑。

避免这种情况的最佳做法是什么?我编写测试用例的方法在某种程度上是错误的吗?

【问题讨论】:

    标签: java unit-testing testing junit tdd


    【解决方案1】:

    为一种方法编写测试时,您必须假设类的其余部分工作正常。如果你不做这个假设,唯一的结论就是每个班级进行一次单一的、大量测试。这不是我们所做的。

    您可以假设该类的其他部分正常工作,因为也会对这些其他部分进行测试,以确保它们的正确性。
    如果某个部分无法正常工作,则测试将失败,向您显示某些地方不正确。
    一旦您的测试套件的测试失败,您就必须修复一个错误。你不能再做任何假设了。

    例子:

    你有一个简单的列表实现,只有三个方法:

    1. 插入
    2. 移除
    3. 计数

    你有三个测试:

    1. 测试insert:
      • 创建列表实例(排列
      • insert 项目(法案
      • 检查count 是否等于1(断言
    2. 测试remove
      • 创建列表实例和insert 项目(排列
      • remove 项目(法案
      • 检查count 是否等于0(断言
    3. 测试count
      • 创建列表实例和insert n 个项目(排列
      • 检索countAct
      • 检查count 是否等于n (断言)

    现在,如果上述任何一个测试失败,你就不能确定你班级的单个成员的正确性:

    • 如果第一个测试失败,第三个测试也将失败。第二个会通过,但实际上并没有测试remove,因为没有什么要删除的。
    • 如果第二个测试失败,其他两个测试仍然会通过。尽管如此,您仍不能确定 insertcount 是否正常工作,因为如果三个成员中的任何一个成员不能正常工作,第二个测试将失败。
    • 如果第三个测试失败,另外两个很可能也会失败。

    失败的测试告诉你一些事情:
    根据失败的测试,您通常可以推断出错误所在。
    示例:如果只有第二个测试失败,而第一个或第三个测试没有失败,则错误很可能在 remove 方法中。

    【讨论】:

    • 我明白你在说什么,但这不会造成方法的循环依赖吗?为了测试插入,我必须使用移除,为了测试移除,我使用插入?这似乎是错误的。可能是我对这些想法没有足够的经验,我很快就会适应不舒服。感谢您的澄清。
    • @crlane:是的,它创建了某种循环依赖。这就是为什么即使一个测试失败,你也不能对你的班级说任何事情。只有当 所有 测试通过时,您才知道一切都按预期工作。
    • 好的,这是有道理的。所以我的错误在于我如何解释单个测试用例的通过。如果单个方法的测试通过,它确实 NOT 意味着该方法是正确的。如果我编写了一个完整的测试套件来正确测试 API 的所有方法,我只能假设我的方法是正确的。这有帮助。再次感谢。
    • @crlane:没错,现在你明白了! :)
    • +1,我要补充一点,您正在测试的其中一件事是方法调用的意外副作用,因此测试对其他方法的依赖不仅可以容忍而且很有用。
    【解决方案2】:

    通常将单元测试视为测试特定功能而不是特定方法会更有效率。任何给定的测试都将检查某些方法集合是否可以正常工作以实现作为测试主题的功能,并且设计良好的测试集中的失败模式往往会告诉您哪个方法很快就崩溃了。

    大量的测试往往会自然而然地脱离 TDD;这是使这项技术如此强大的原因之一。如果我写的是Deque,我写的测试往往会如下,一般按这个顺序呈现。

    1. empty_Deque_isEmpty -- 实现 isEmpty 以始终返回 true
    2. non_empty_Deque_isntEmpty -- 实现insertFirst 使isEmpty 实例变量为false
    3. re_emptied_Deque_isEmpty -- 将isEmpty 使用的实例变量更改为响应insertFirstremoveFirst 的数字
    4. is_empty_Deque_size_correct -- 实现 size 始终返回 0
    5. is_nonempty_Deque_size_correct -- 添加实例变量以跟踪大小;意识到它正在做与isEmpty 相同的事情;重构
    6. is_re_emptied_Deque_size_correct -- 让测试通过,因为我们做了什么来实现 5. 发生
    7. does_removing_from_empty_Deque_throw -- removeFirst 需要检查 size 在做任何其他事情之前
    8. is_inserted_item_returned -- insertFirstremoveFirst 现在填充 T 实例变量
    9. is_inserted_item_returned_from_end -- 添加removeLastremoveFirst 的副本;重构
    10. is_rear_inserted_item_returned -- 添加复制insertFirstinsertLast;重构
    11. are_all_inserted_items_returned -- 更改 insertFirstremoveFirst 以作用于 SomeKindOfCollection<T>;注意不检查检索顺序
    12. does_removeFirst_retrieve_items_in_correct_order -- 插入两个东西,确保第二个由removeFirst 返回。可能已经是真的了。
    13. does_removeLast_retrieve_items_in_correct_order -- removeLast 同上,除非很确定还没有通过。

    这是一大堆测试,但是当您查看它们时,您应该会注意到其中的模式。这些测试都不是真正的“count 测试”或“removeFirst 测试”。但是当我们完成时,该类的整个接口都在运行,并且该接口所需的所有内部组件都已开发完毕。一些测试依赖于不止一种方法,如果该方法失败,它们都会中断。但是中断的模式往往会非常有助于确定错误在哪里。

    同样有趣的是,我们可以通过多少这些测试而无需承诺在对象中实际拥有一个集合,这表明可以将该组测试分解为更通用的测试套件,这将是有用的开发时PriorityQueue

    【讨论】:

    • +1 我完全同意“测试特定功能而不是特定方法”。我虽然也想把它带入我的答案,但我觉得它会压倒 OP。很高兴你提出来!
    猜你喜欢
    • 2017-08-09
    • 2012-11-12
    • 2021-05-15
    • 1970-01-01
    • 2018-09-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多