2013-02-20 52 views
7

我想了解更多关于JUnit和TDD,但我遇到了一些测试用例之间耦合的问题。减少测试用例之间的耦合

当我为特定数据类型的API编写测试用例时,说一个Deque<T>,如何限制测试用例之间的耦合?举例来说,如果我的方法insertFirst(T item)编写测试用例,它看起来非常简单的假设,我应该能够调用该方法正确初始化的对象之后,触发两件事情:

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

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

避免这种情况的最佳做法是什么?我以某种方式编写测试用例是否错误?

回答

12

当为一种方法编写测试时,假设其余的类都工作正常。如果你不做这个假设,唯一的结论将是每个班级的单个测试,大量的测试。这不是我们所做的。

您可以假设该类的其他部分正常工作,因为也会对其他部分进行测试,以确保其正确性。
如果一部分工作不正常,测试将失败,向您显示某些内容不正确。
只要测试套件的测试失败,就需要修复一个错误。你不能再做任何假设。

例子:

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

  1. 插入
  2. 删除

你有三个测试:

  1. 试验insert
    • 创建列表的实例(安排
    • insert项目(
    • 检查count等于1(断言
  2. 测试remove
    • 创建列表和insert项目的实例(安排
    • remove项目(
    • 检查count等于0(断言
  3. 试验count
    • 创建列表实例和insert n项(安排
    • 检索count
    • 检查count等于n(断言)现在

,如果上述任何测试失败,你不能请确保您的班级的单个成员的正确性:

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

的失败测试告诉你一件事,但:
根据失败的测试,你经常可以扣除在误差不得。
示例:如果只有第二个测试失败,但不是第一个或第三个测试,则错误最可能在remove方法中。

+1

我明白你在说什么,但是这不会创建方法的循环依赖吗?为了测试插入,我必须使用remove,并且为了测试remove,我使用insert?不知何故,这似乎是错的。这可能是因为我对这些想法没有足够的经验,我很快就会因为不舒服而变得舒适。感谢您的澄清。 – crlane 2013-02-20 13:59:08

+1

@crlane:是的,它创建了某种循环依赖。这就是为什么即使只有一次测试失败,你也无法说出你的课程*的任何事情。只有*全部*测试通过,你才知道一切都按预期工作。 – 2013-02-20 14:00:20

+0

@crlane:请参阅具体示例的更新答案。 – 2013-02-20 14:12:32

4

将单元测试视为测试特定功能而不是特定方法通常会更有成效。任何给定的测试都将检查某些方法集合是否正确地工作以实现作为测试主题的功能,而设计良好的测试集中的失败模式将倾向于告诉您哪种方法相当快地中断。

一个好的测试集合倾向于自然地脱离TDD;这是使技术如此强大的一件事情。如果我正在编写Deque,我写的测试将倾向于如下,通常按此顺序呈现。

  1. empty_Deque_isEmpty - 实施isEmpty总是返回true
  2. non_empty_Deque_isntEmpty - 实施insertFirst使isEmpty实例变量假
  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 - 添加removeLast那就是removeFirst复印件;重构
  10. is_rear_inserted_item_returned - 添加insertLast那份insertFirst;重构
  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时很有用。

+0

+1我完全同意“测试特定功能而不是特定方法”。我虽然也想把它带入我的答案,但我感觉它会压倒OP。很高兴你提起它! – 2013-02-21 21:52:14