如何提倡对私有代码进行单元测试?


15

我试图在工作组中倡导单元测试,但我经常遇到的反对意见是,它只能用于外部导出的API(这只是我们系统的最小和非关键部分),而不能用于内部和专用代码(现在仅具有功能测试)。

虽然我认为单元测试可以并且应该应用于所有代码,但是我如何说服我的同事?


3
如果您有需要测试的私有方法,则通常表明您的代码违反了SRP,并且那里还有另一个类在喊叫自己进行提取和测试。
Paddyslacker

@Paddyslacker:我觉得所有代码都需要测试。我不明白为什么遵循单一职责原则的代码单元不应该进行单元测试……
Wizard79 2010年

4
@lorenzo,你错过了我的观点;也许我做得不好。如果将这些私有方法提取到另一个类,则现在需要可以从原始类访问它们。由于这些方法现已公开,因此需要进行测试。我并不是在暗示不应该对它们进行测试,我是在暗示,如果您觉得需要直接测试这些方法,则可能它们不应该是私有的。
Paddyslacker 2010年

@Paddyslacker:我觉得也需要直接测试私有方法。您为什么认为他们不应该是私人的?
Wizard79 2010年

6
通过测试私有方法,您正在破坏抽象。您应该在单元测试中测试状态和/或行为,而不是实现。您的示例/场景应该能够验证私有代码的结果-如果您发现困难,那么正如Paddyslacker所说,这可能意味着您违反了SRP。这也可能意味着尽管您还没有提炼出示例来真正代表您的代码在做什么。
FinnNk 2010年

Answers:


9

您的同事可能会将真实的单元测试与集成测试混淆。如果您的产品是(或具有)API,则可以将集成测试编程为NUnit测试用例。有些人错误地认为那是单元测试。

您可以尝试通过以下方式说服您的同事(我确定您已经知道这些知识,我只是想指出的是向您的同事指出可能会有所帮助):

  • 测试覆盖率。测量那些集成测试的实际测试覆盖率。对于从未进行过测试的人员,这是一项现实检查。由于在几层输入之外很难执行所有逻辑路径,因此测试覆盖率最高可达20%到50%之间。为了获得更多的报道,您的同事需要编写真实的,独立的单元测试。
  • 配置。部署相同的被测软件,也许您可​​以向您的同事演示在不同环境中运行他们的测试有多困难。各种文件,数据库连接字符串,远程服务的URL等的路径-所有这些都加起来了。
  • 执行时间。除非测试是真正的单元测试并且可以在内存中运行,否则它们将需要大量时间才能运行。

12

在内部/私有代码上使用单元测试的原因与外部支持的API完全相同:

  • 它们可防止错误再次发生(单元测试构成回归测试套件的一部分)。
  • 他们记录(以可执行文件格式!)该代码有效。
  • 它们提供了“代码有效”含义的可执行定义。
  • 它们提供了一种自动化的方法来证明代码确实符合规范(如上一点所定义)。
  • 它们显示了在出现意外输入的情况下单元/类/模块/功能/方法如何失败。
  • 他们提供了有关如何使用该单元的示例,这对于新团队成员而言是很好的文档。

8

如果您按照我认为的意思来表示私密,那么不可以-不应该对它进行单元测试。您只能进行单元测试的可观察的行为/状态。您可能会错过TDD的“红绿重构”循环背后的要点(并且,如果您不先进行测试,则适用相同的原理)。编写测试并通过测试后,您不希望它们在执行重构时进行更改。如果您被迫对私有功能进行单元测试,则可能意味着围绕公共功能的单元测试存在缺陷。如果围绕公共代码编写测试既困难又复杂,那么您的类可能做得太多,或者您的问题没有明确定义。

更糟糕的是,随着时间的流逝,您的单元测试将变成一团糟,使您的速度降低,却丝毫不增加任何价值(更改实现,例如优化或删除重复项,对单元测试不会产生任何影响)。但是,应该对内部代码进行单元测试,因为行为/状态是可以观察到的(仅以受限的方式)。

当我第一次进行单元测试时,我花了各种各样的技巧对私有的东西进行单元测试,但是现在,在我的带领下,几年了,我认为这比浪费时间更糟糕。

这是一个愚蠢的例子,当然,在现实生活中,您将拥有比这些更多的测试:

假设您有一个返回字符串排序列表的类-您应该检查结果是否排序,而不是它对列表的实际排序方式。您可以使用仅对列表进行排序的单个算法来开始实施。完成后,如果您随后更改排序算法,则无需更改测试。此时,您只有一个测试(假设排序已嵌入您的类中):

  1. 我的结果排序了吗?

现在说您想要两种算法(在某些情况下也许一种算法更有效,而在其他情况下则不是),那么每种算法都可以(并且通常应该)由不同的类提供,并且您的类从中选择—您可以检查这种情况是否正在发生您使用模拟程序选择的场景,但是您的原始测试仍然有效,并且由于我们仅验证可观察的行为/状态,因此无需更改。您最终进行了3个测试:

  1. 我的结果排序了吗?
  2. 给定一个场景(假设初始列表几乎从头开始排序)是否调用了使用算法X对字符串进行排序的类?
  3. 给定一个方案(初始列表按随机顺序排列)是否调用了使用算法Y对字符串排序的类?

另一种选择是开始在您的类中测试私有代码-您从中什么也没有得到-上述测试告诉我就单元测试而言我需要知道的一切。通过添加私人测试,您可以为自己制造一件直筒外套,如果您不仅检查结果的排序方式,还检查结果的排序方式,会做多少工作?

(这种类型的)测试仅应在行为发生变化时才更改,开始针对私有代码编写测试,并且超出了测试范围。


1
也许对“私有”的含义存在误解。在我们的系统中,99%的代码是“私有”的,那么我们有一个小的API用于自动化/远程控制系统的组件之一。我的意思是对所有其他模块的代码进行单元测试。
Wizard79 2010年

4

这是另一个原因:在假设的情况下,我将不得不在对外部API与私有部分进行单元测试之间进行选择,而我会选择私有部分。

如果每个私有部分都被测试覆盖,则包含这些私有部分的API也应覆盖几乎100%,仅覆盖上层。但这可能只是薄薄的一层。

另一方面,仅测试API时,很难完全覆盖所有可能的代码路径。


+1“另一方面……”但是,如果没有其他问题,请添加测试以最大程度地避免失败。
托尼·恩尼斯

2

人们很难接受单元测试,因为这似乎是浪费时间(“我们可能正在编写另一个赚钱的项目!”)或递归(“然后我们必须为测试用例编写测试用例!”)两者都说我很内gui。

第一次发现错误时,必须面对一个事实,即您并不完美(程序员很快忘记了它!),然后走了,“嗯”。


单元测试的另一个方面是必须将代码编写为可测试的。意识到某些代码很容易测试,而某些代码却不是一个好的程序员,所以他们会选择“嗯”。


您是否问过同事为什么单元测试仅对面向外部的API有用?


展示单元测试价值的一种方法是等待一个讨厌的错误发生,然后展示单元测试如何阻止它。这并不是要在他们的脸上摩擦它,而是要在他们的脑海中将单元测试从象牙塔理论转变为现实中的现实。

另一种方法是等到同一错误发生两次。“恩,老板,我们添加了代码以测试上周的问题后是否为空,但是这次用户输入的内容是空的!”


以身作则。为您的代码编写单元测试,然后向老板展示价值。然后看看老板是否有一天会叫披萨吃午餐并做一个介绍。


最后,我无法告诉您当我们要推向产品时所感到的缓解,并且单元测试中出现了一个绿色的条。


2

有两种类型的私有代码:私有代码被公共代码调用(或私有代码,获取由私人代码调用得到由公共代码(或...)调用)和专用代码,没有最终会被公众称为码。

前者已经通过测试以测试公共代码。后者根本无法调用因此应删除而不进行测试。

请注意,在执行TDD时,不可能存在未经测试的私人代码。


在我们的系统中,有99%的代码属于第三类:私有的,未被公共代码调用,并且对于系统是必不可少的(我们系统的一小部分具有外部的公共API)。
Wizard79 2010年

1
“请注意,当您执行TDD时,不可能存在未经测试的私人代码。” <-删除一个测试用例,而不知道该测试是覆盖特定分支的唯一测试。好的,那是更多的“目前未经测试”的代码,但是很容易看到稍后进行的细微重构更改了该代码……仅测试套件不再涵盖它。
Frank Shearar 2010年

2

单元测试与测试代码单元有关。由您决定什么是单位。您的同事将单位定义为API元素。

无论如何,测试API也会导致行使私有代码。如果将代码覆盖率定义为单元测试进度的指标,则最终将测试所有代码。如果代码的某些部分尚未到达,请给您的同事三个选择:

  • 定义另一个覆盖该部分的测试用例,
  • 分析代码以证明为什么它不能在单元测试的上下文中涵盖,而在其他情况下应该涵盖,
  • 删除没有覆盖的没有理由的无效代码。

在我们的系统中,API只是其中的一小部分,它允许对第三方应用程序进行自动化/远程控制。仅测试API即可覆盖1%的代码覆盖率...
Wizard79 2010年
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.