【问题标题】:TDD - writing tests for a method that iterates / works with collectionsTDD - 为迭代/使用集合的方法编写测试
【发布时间】:2012-07-05 22:22:07
【问题描述】:

作为 TDD 的新手,我正在努力编写处理集合的单元测试。例如,目前我正在尝试提出一些测试方案来基本上测试以下方法

int Find(List<T> list, Predicate<T> predicate);

该方法应返回列表list 中与谓词predicate 匹配的第一项的索引。到目前为止,我能够提出的唯一测试用例是

  • list 不包含任何项目时 - 返回-1
  • list 包含 1 个匹配 predicate 的项目时 - 返回 0
  • list 包含 1 个与 predicate 不匹配的项目时 - 返回 -1
  • list 包含两个匹配predicate 的项目时 - 返回0
  • list 包含2 个项目,其中第一个匹配predicate - 返回0
  • 等等……

如您所见,这些测试用例数量众多,无法令人满意地测试我真正想要的实际行为。我内心的数学家想要做某种 TDD-by-induction

  • list 不包含任何项目时 - 返回-1
  • list 包含 N 项时,对第一项调用 predicate,然后对剩余的 N-1 项递归调用 Find

但是这会引入不必要的递归。对于上述方法,我应该在 TDD 中编写什么样的测试用例?


顺便说一句,我真正尝试测试的方法只是Find,仅用于特定的集合和谓词(我可以独立编写测试用例)。当然,我应该有一种方法可以避免编写上述任何测试用例,而是简单地测试该方法是否使用正确的参数调用了其他一些 Find 实现(例如 FindIndex)?

请注意,无论如何我仍然想知道如何我可以对Find(或其他类似的方法)进行单元测试,即使事实证明在这种情况下我没有需要。

【问题讨论】:

  • 你用的是什么单元测试框架?
  • @Kevin Rhino 模拟 + MSTest / Visual Studio 单元测试框架

标签: c# collections tdd


【解决方案1】:

如果 find() 正在工作,那么它应该返回与谓词匹配的第一个元素的索引,对吧?

因此,您需要针对空列表案例进行测试,针对不匹配元素案例进行一项测试,针对匹配元素案例进行一项测试。我会觉得这就足够了。在 TDDing find() 的过程中,我可能会编写一个特殊的第一个元素传递案例,我可以很容易地伪造它。我可能会写:

emptyListReturnsMinusOne()
singlePassingElementReturnsZero()
noPassingElementsReturnsMinusOne()
PassingElementMidlistReturnsItsIndex()

并且期望这个顺序会推动我正确的实现。

【讨论】:

  • 您可能想要添加一个PassingElementAtEndOfListReturnsItsIndex() 测试。
  • 我很难想象一个现实的解决方案可以通过 midlist 测试但未通过 endOfList 测试。
  • 我可以想象有人决定 for 循环比 foreach 更快并引入一个错误的情况。那个人当然是个白痴,但我个人为白痴编写测试(结果通常我是白痴:))
  • 实际上在五分钟后认为单实例情况会捕捉到这一点,但我仍然认为测试所有边界情况很有用。
【解决方案2】:

当恐惧被无聊取代时停止测试 - 肯特贝克

在这种情况下,给定通过测试的概率是多少

  • “当列表包含 2 项都匹配谓词时 - 返回 0”

下面的测试会失败吗?

  • "当列表包含 5 项都匹配谓词时 - 返回 0"

我会写前者,因为我担心这种行为不适用于多个元素。但是,一旦 2 工作,为 5 编写另一个只是乏味(​​除非在生产代码中硬编码假设 2.. 它应该被重构掉。即使不是,我也只需将现有测试修改为有 5 个而不是 2 个,并使其适用于一般情况)。

因此,为明显不同的事物编写测试。在这种情况下,列出(零、一、多个)元素和(包含/不包含)操作数

【讨论】:

    【解决方案3】:

    根据您对Find 方法的要求,我将测试以下内容:

    1. listnull - 抛出 ArgumentNullException 或返回 -1
    2. list 不包含任何项目 - 返回 -1
    3. predicatenull - 抛出 ArgumentNullException 或返回 -1
    4. list 包含一项与 predicate 不匹配的项目 - 返回 -1
    5. list 包含一项与 predicate 匹配的项目 - 返回 0
    6. list 包含多个项目,但没有项目与 predicate 匹配 - 返回 -1
    7. list 包含与 predicate 匹配的多个项目 - 返回第一个匹配项的索引

    基本上,您将首先测试结束情况 - 空参数、空列表。之后,进行一项测试。最后,测试多个项目的匹配和不匹配。

    对于 null 参数,您可以抛出异常或返回 -1,具体取决于您的偏好。

    【讨论】:

      【解决方案4】:

      不要更改列表,更改谓词

      考虑如何调用该方法。当有人调用Find 方法时,他们已经有了一个列表并且需要考虑谓词。所以想出一些能证明Find行为的好例子:

      示例: 对所有测试用例使用相同的列表3, 4 可以很容易理解:

      1. 谓词&lt; 5 匹配两个数字(返回1
      2. 谓词== 3 匹配3(返回0
      3. 谓词 == 0 不匹配(返回 -1

      这确实是您指定行为所需的全部内容,并且通过更改谓词而不是列表,您可以给出如何使用Find 方法的好例子。包含零个、一个或两个元素的列表并没有真正改变Find 的行为,也没有真正改变该方法的使用方式。对您的测试用例遵循 DRY,专注于指定行为而不是证明代码是正确的,否则您最终将花费所有时间编写测试。

      【讨论】:

        【解决方案5】:

        试图回答你的问题:我没有任何 Rhino 模拟的经验,但我相信它应该有类似于 FakeItEasy(?) 的东西:

        var finder = A.Fake<IMyFindInterface>();
        
        // ... insert code to call IMyFindInterface.Find(whatever) here
        
        A.CallTo(() => finder.find(A<List>.That.Matches(
                          x => x.someProperty == someValue))).MustHaveHappened();
        

        通过将 Find() 的实现放在接口后面,然后将使用该接口的方法传递一个假的,您可以检查该方法是否使用某些参数调用。 (如果预期调用未完成,MustHaveHappended() 将导致测试失败)。

        由于您知道 IMyFindInterface 的真正实现只是将调用传递给您已经信任的实现,因此这应该是一个足够好的测试来验证您正在测试的代码是否调用了 Find-以正确的方式实施。

        只要您只是想确保您的代码(您正在测试的单元)通过抽象出该组件本身以正确的方式调用您已经信任的某个组件,就可以使用相同的过程 - 这正是我们在单元测试时想要的.

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2021-12-01
          • 1970-01-01
          • 2018-10-25
          • 1970-01-01
          • 1970-01-01
          • 2017-08-26
          • 1970-01-01
          相关资源
          最近更新 更多