如何对返回集合的测试方法进行单元化,同时避免测试中的逻辑


14

我正在测试一种生成数据对象集合的方法。我想验证是否正确设置了对象的属性。一些属性将被设置为相同的东西;其他的将被设置为一个值,该值取决于它们在集合中的位置。做到这一点的自然方法似乎是循环。但是,Roy Osherove强烈建议不要在单元测试中使用逻辑(Art of Unit Testing,178)。他说:

包含逻辑的测试通常一次只能测试多个对象,因此不建议这样做,因为该测试的可读性和脆弱性更高。但是测试逻辑也会增加复杂性,其中可能包含隐藏的错误。

通常,测试应该是一系列没有控制流的方法调用,甚至没有try-catch,并且带有断言调用。

但是,我看不出我的设计有什么问题(您还如何生成数据对象列表,其中某些值取决于它们在序列中的位置?无法完全分别生成和测试它们)。我的设计中是否存在非测试友好的问题?还是过于刻板地致力于Osherove的教学?还是有一些我不知道的秘密单元测试魔术可以解决这个问题?(我正在用C#/ VS2010 / NUnit编写,但是如果可能的话,寻找与语言无关的答案。)


4
我建议不要循环播放。如果您的测试是第三件事的Bar设置为Frob,则编写测试以专门检查第三件事的Bar是Frob。那本身就是一个测试,直接进行,没有循环。如果您的测试是要收集5件物品,那也是一项测试。这并不是说您永远不会有循环(显式或其他方式),只是您不需要经常循环。另外,将Osherove的书视为比实际规则更多的指导原则。
Anthony Pegram

1
@AnthonyPegram集合是无序的-Frob有时可能是第三,有时可能是第二。in如果测试为“ Frob已成功添加到现有集合中”,则您不能依赖它,而必须进行循环(或类似Python的语言功能)。
2013年

1
@Izbata,他的问题专门提到排序很重要。他说:“其他人将被设置为一个值,这取决于他们在集合中的位置。” 在C#(他引用的语言)中有很多按插入顺序排序的集合类型。为此,您还可以依赖于您提到的Python中列表的顺序。
Anthony Pegram

另外,假设您正在测试集合上的Reset方法。您需要遍历集合并检查每个项目。根据集合的大小,不进行循环测试是很荒谬的。或者说我正在测试一些应该增加集合中每个项目的项目。您可以将所有项目设置为相同的值,调用增量,然后检查。那测试糟透了。您应该将其中几个设置为不同的值,调用增量,并检查所有不同的值是否正确增量。仅检查集合中的一个随机项目会给您带来很多机会。
iheanyi 2014年

我不会以这种方式回答,因为我将获得大量专心致志的选票,但我经常只是toString()收藏,并与应有的东西进行比较。简单而有效。
user949300 '19

Answers:


16

TL; DR:

  • 编写测试
  • 如果测试做得太多,那么代码也可能做得太多。
  • 它可能不是单元测试(但不是不好的测试)。

测试的第一件事是关于教条无益。我喜欢阅读《 Test of Testivus》,该书轻松地指出了教条的一些问题。

编写需要编写的测试。

如果测试需要以某种方式编写,请以这种方式编写。试图将测试强制为某种理想的测试布局,或者根本不进行测试,这不是一件好事。今天进行测试可以胜过稍后进行“完美”测试。

我还将指出丑陋的测试:

当代码很丑时,测试可能很丑。

您不喜欢编写丑陋的测试,但是丑陋的代码需要测试最多。

不要让丑陋的代码阻止您编写测试,而要让丑陋的代码阻止您编写更多测试。

对于那些长期关注的人来说,这些可以说是不言而喻的……他们只是在思考和编写测试的方式中变得根深蒂固。对于尚未去过并试图达到这一点的人,提醒可能会有所帮助(我什至发现重新阅读它们有助于避免陷入某些教条中)。


在编写丑陋的测试时,请务必考虑一下,如果是代码,则可能表明该代码正试图做太多事情。如果要测试的代码太复杂而无法通过编写简单的测试来正确执行,则您可能需要考虑将代码分解为较小的部分,可以使用较简单的测试进行测试。一个人不应该编写一个可以做所有事情的单元测试(那时可能不是单元测试)。就像“上帝的对象”是坏的一样,“上帝的单元测试”也很糟糕,应该作为回头再看代码的指示。

通过这样的简单测试,您应该能够在合理的范围内执行所有代码。做得更多的端对端测试涉及更大的问题(“我有这个对象,将其编组为xml,通过规则发送到Web服务,先退出后再取消编组”),这是一个很好的测试-但肯定不是不是单元测试(并且属于集成测试领域-即使它具有模拟调用的服务并在内存数据库中自定义以进行测试)。它可能仍使用XUnit框架进行测试,但是测试框架并未使其成为单元测试。


7

我要添加一个新答案,因为我的观点与撰写原始问答时的观点有所不同;将它们分成一个网状是没有意义的。

我在原始问题中说

但是,我看不出我的设计有什么问题(您还如何生成数据对象列表,其中某些值取决于它们在序列中的位置?无法完全分别生成和测试它们)

这是我做错的地方。在进行了去年的函数编程之后,我现在意识到我只需要使用累加器进行收集操作。然后,我可以将我的函数编写为对一件事情进行操作的纯函数,并使用一些标准库函数将其应用于集合。

所以我的新答案是:使用函数式编程技术,您将在大多数时间完全避免此问题。您可以编写函数以对单个事物进行操作,并且仅在最后一刻将其应用于事物集合。但是,如果它们是纯净的,则可以在不引用集合的情况下对其进行测试。

对于更复杂的逻辑,请依靠基于属性的测试。当它们确实具有逻辑时,它应该小于并与被测代码的逻辑相反,并且每个测试比基于案例的单元测试所验证的要多得多,因此少量的逻辑是值得的。

最重要的是,始终取决于您的类型。获得最强的类型,并利用它们来发挥自己的优势。这将减少您必须首先编写的测试数量。


4

不要试图一次测试太多的东西。对于一个测试,集合中每个数据对象的每个属性都太多了。相反,我建议:

  1. 如果集合是固定长度的,则编写一个单元测试以验证长度。如果它是可变长度,则编写多个测试以测试可表征其行为的长度(例如0、1、3、10)。无论哪种方式,都不要在这些测试中验证属性。
  2. 编写单元测试以验证每个属性。如果集合是固定长度且较短的集合,则针对每个测试针对每个元素的一个属性进行断言。如果它是固定长度但很长,则选择一个代表性但较小的元素样本,以针对每个属性进行断言。如果它是可变长度的,则生成一个相对较短但有代表性的集合(即可能是三个元素),并针对每个元素的一个属性进行断言。

这样做可以使测试足够小,以至于省去了循环的痛苦。C#/单元示例,给定测试方法ICollection<Foo> generateFoos(uint numberOfFoos)

[Test]
void generate_zero_foos_returns_empty_list() { ... }
void generate_one_foo_returns_list_of_one_foo() { ... }
void generate_three_foos_returns_list_of_three_foos() { ... }
void generated_foos_have_sequential_ID()
{
    var foos = generateFoos(3).GetEnumerable();
    foos.MoveNext();
    Assert.AreEqual("ID1", foos.Current.id);
    foos.MoveNext();
    Assert.AreEqual("ID2", foos.Current.id);
    foos.MoveNext();
    Assert.AreEqual("ID3", foos.Current.id);
}
void generated_foos_have_bar()
{
    var foos = generateFoos(3).GetEnumerable();
    foos.MoveNext();
    Assert.AreEqual("baz", foos.Current.bar);
    foos.MoveNext();
    Assert.AreEqual("baz", foos.Current.bar);
    foos.MoveNext();
    Assert.AreEqual("baz", foos.Current.bar);
}

如果您习惯于“平面单元测试”范式(无嵌套结构/逻辑),那么这些测试似乎很干净。因此,通过将原始问题识别为试图一次测试太多的属性而不是缺少循环,可以避免测试中的逻辑。


1
Osherove会让您大吃一惊,因为您拥有3条断言。;)第一个失败意味着您永远不会验证其余的。还请注意,您并没有真正避免循环。您只是将其显式扩展为执行的形式。这不是一个硬性的批评,而是建议采取更多的措施,将您的测试用例尽可能地减少,在出现故障时给自己更具体的反馈,同时继续验证可能仍会通过(或失败)的其他情况。他们自己的具体反馈)。
Anthony Pegram

3
@AnthonyPegram我知道每次测试一个断言的范例。我更喜欢“测试一件事”的口头禅(正如鲍勃·马丁所倡导的,反对“每次测试一次断言”,在“ 清洁代码”中)。旁注:具有“期望”与“断言”的单元测试框架很好(Google测试)。至于其余的,为什么不通过示例将建议分解成一个完整的答案?我想我可以受益。
卡扎尔克
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.