TDD - 为迭代/处理集合的方法编写测试

14

作为一个TDD的新手,我在编写处理集合的单元测试方面遇到了困难。例如,目前我正在尝试想出一些测试场景,以基本测试以下方法:

int Find(List<T> list, Predicate<T> predicate);
该方法应返回匹配断言 predicate 的列表 list 中第一个项目的索引。目前我唯一能想到的测试用例是:
  • list不包含任何项目时 - 返回-1
  • list包含1个与predicate匹配的项目 - 返回0
  • list包含1个不符合predicate的项目 - 返回-1
  • list包含2个都与predicate匹配的项目 - 返回0
  • list包含2个项目,第一个匹配predicate - 返回0
  • 等等...

然而,正如您所见这些测试用例数量众多且无法令人满意地测试实际上我想要的行为。作为数学家,我想进行某种归纳TDD

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

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


顺便说一下,我要测试的方法确实只是Find,只是针对特定的集合和断言(我可以独立编写测试用例)。 肯定有一种方法可以避免编写以上任何测试用例,而是简单地测试该方法是否使用正确的参数调用了其他Find实现(例如FindIndex)吗?

请注意,在任何情况下,即使在这种情况下我不需要编写上述任何测试用例,我仍希望知道如何对Find(或类似的另一个方法)进行单元测试。


1
你正在使用哪个单元测试框架? - Kevin Aenmey
@Kevin Rhino嘲笑+MSTest/Visual Studio单元测试框架 - Justin
5个回答

9
如果find()方法起作用,它应该返回第一个与谓词匹配的元素的索引,对吗?
因此,您需要对空列表情况、无匹配元素情况和匹配元素情况进行测试。我认为这样足够了。在TDD find()方法的过程中,我可能会编写一个特殊的第一个元素通过的案例,并且可以很容易地伪造它。我可能会写下如下代码:
emptyListReturnsMinusOne()
singlePassingElementReturnsZero()
noPassingElementsReturnsMinusOne()
PassingElementMidlistReturnsItsIndex()

我期望这个序列会推动我正确地实现它。

2
你可能想要添加一个 PassingElementAtEndOfListReturnsItsIndex() 测试。 - Philipp
1
很难想象出一个现实的解决方案,它能通过中间测试但却无法通过结尾测试。 - Carl Manaster
1
我可以想象这样一种情况,即有人决定使用for循环比foreach更快,并引入了一个偏移量错误。当然,那个人会是个白痴,但就我个人而言,我会为白痴编写测试(通常是我自己成为白痴的时候 :) )。 - David Hall
1
实际上,经过五分钟的思考,单个实例情况应该可以捕捉到这一点,但我仍然认为测试所有边界情况是有用的。 - David Hall

2
当恐惧被无聊替代时,请停止测试 - Kent Beck
在这种情况下,如果给定“当列表包含2个匹配谓词的项时 - 返回0”的测试通过,则以下测试会失败的概率是多少?
“当列表包含5个匹配谓词的项时 - 返回0”。
我写前者是因为我担心该行为不适用于多个元素。然而,一旦2个元素可行,再为5个元素编写另一个测试就变得很乏味了(除非生产代码中有硬编码的假设..应进行重构。即使没有,我也会修改现有测试,将2改为5,并使其适用于通用情况)。
因此,要针对明显不同的内容编写测试。在这种情况下,列表具有(零、一、多个)元素和(包含/不包含)运算符。

0

基于您对Find方法的要求,以下是我所测试的:

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

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

对于null参数,您可以根据自己的喜好选择抛出异常或返回-1


0

不要改变列表,改变谓词

考虑方法将如何被调用。当有人调用Find方法时,他们已经有一个列表并需要考虑谓词。因此,请考虑一些良好的示例来演示Find的行为:

例如: 对于所有测试用例使用相同的列表3, 4使其易于理解:

  1. 谓词<5匹配两个数字(返回1
  2. 谓词==3匹配3(返回0
  3. 谓词==0不匹配任何一个(返回-1

这实际上是您需要指定行为的全部内容,通过更改谓词而不是列表,您可以提供如何使用Find方法的良好示例。 零个、一个或两个元素的列表并不真正改变Find的行为,也不是该方法的实际使用方式。 在测试用例中遵循DRY原则,专注于指定行为而不是证明代码正确性,否则您将花费所有时间编写测试。


0
尝试回答你的问题:我没有使用过Rhino Mocks,但我相信它应该有类似于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-implementation。
每当您只想确保您的代码(您正在测试的单元)以正确的方式调用一些组件时,都可以使用相同的过程,通过抽象化该组件本身来实现 - 这正是我们进行单元测试时所希望的。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接