编写“可单元测试”的代码?

40

您使用什么样的实践方法来使您的代码更加适合进行单元测试?

19个回答

61
  • TDD -- 先编写测试,迫使您考虑可测试性并帮助编写实际需要的代码,而不是您认为可能需要的

  • 重构成接口 -- 使得模拟更容易

  • 如果不使用接口,则将公共方法设为虚方法--使得模拟更容易

  • 依赖注入 -- 使得模拟更容易

  • 更小、更具针对性的方法 -- 测试更加专注和易于编写

  • 避免使用静态类

  • 除非必要,否则避免使用单例

  • 避免使用封闭类


1
避免单例模式,除非必要。我在十年的编程经验中还没有遇到必须使用单例模式的情况。 - Ben Barkay

14

依赖注入似乎有所帮助。


1
是的,那也是我的最爱建议——这样,你的单元测试可以轻松地替换注入的任何依赖项为模拟对象。 - Alex Martelli
2
我认为依赖注入很关键(手动方式也可以,您不需要使用 DI 框架)。如果没有它,大多数测试都会强制成为集成测试而不是单元测试。 - Jamie Ide
@Jamie - 是的,我发现对于我的用途,“手动”是可以的。 - dss539

11

先写测试 - 这样,测试可以推动你的设计。


8
  1. 使用测试驱动开发(TDD)
  2. 在编写代码时,尽可能利用依赖注入
  3. 针对接口进行编程,而不是具体的类,以便可以替换模拟实现。

2
@Visage:出于好奇,如果您能够使用TDD,但并非在所有地方都使用依赖注入,并且在许多地方使用具体类,并且不总是使用模拟;做了所有这些坏事,但最终代码覆盖率达到90%或更高,那么这会使您成为一个坏人吗?还是意味着并非所有其他东西都是真正必要的质量? - John Saunders
有趣的问题。如果没有这些东西,进行IME单元测试会变得非常困难,我想知道90%的覆盖率是否是能够测试的90%,而不是需要测试的90% ;) - PaulJWilliams
@Michael:在实际操作中,实施DI、mock等没有任何问题,这是为了完成测试并保持其可维护性。我遵循红-绿-重构规则,因此我不断地重构测试和代码;如果DI会改善某些测试,那么我会采用DI。 - John Saunders
@John - 你的单元测试中一个“单元”有多大?是1个方法吗?还是1个调用了12个其他方法的方法?你是否有一个单元测试覆盖超过100行代码的单元测试?依赖注入允许测试非常小的单元,而且将DI添加到旧代码中相当容易。如果你能够轻松地在没有DI的情况下测试小单元,那么我很钦佩你的设计技巧,并且可以向你学习一些东西。但是,如果你每次测试都测试大块的代码,那么你就有点不好意思了。:P - dss539
@dss539:我是一只老狗,五年前才学会这个技巧,所以我不会那样做(基于每种方法的测试等)。就像贝克(注:指极限编程之父Kent Beck),我选择要实现的功能模块,并开始编写失败的测试。如果一次性太多了,我会为较小的块编写测试。在成功测试重构后,它们可能会被合并成更大的测试,但直到它们单独通过为止。 - John Saunders
显示剩余2条评论

7

确保您的所有类都遵循单一职责原则。单一职责意味着每个类应该只有一个职责。这样可以使单元测试更加容易。


6

我知道我的言论可能会被贬低,但我还是要发表一下自己的意见 :)

虽然这里提出的建议大多数都很好,但我认为需要适当地加以节制。目标是编写更健壮、易于更改和可维护的软件。

目标不是编写可单元测试的代码。尽管编写可测试的代码很重要,但在实现目标的过程中,投入了大量的精力去实现可测试性。它听起来很好,肯定会让人感到温暖和舒服,但事实上所有这些技术、框架、测试等都有成本。

它们需要消耗时间进行培训、维护,还会增加生产力开销等等。有时值得,有时不值得,但您永远不应该带着盲目的心态,只为了使代码更具有“可测试性”而前进。


2
+1 是因为常识告诉我们,代码经过测试后更易于修改和维护;-1 是因为测试的本质使得代码变得更加易变。 - dss539
2
+1 为了保持理智!单元测试很好,但如果测试用于覆盖 所有 内容,那么 TDD 对于快速完成任务来说是一个巨大的消耗。如果您对类似获取器/设置器之类的琐碎或样板式代码进行测试,那么更有可能由于接口更改而导致测试失败,而不是实际错误。聪明的单元测试是好的,过度热衷的单元测试会导致脆弱的代码和缓慢的开发。 - Jacob
如果你正在编写大量的样板代码,也许是时候找一种新的语言了。 - dss539
2
+1 鼓励大家保持常识,时刻动脑筋。但我写单元测试并不是为了让自己感到聪明和写出复杂的代码,而是因为它可以使我的代码更易于修改和维护,并且我知道它是有效的。如果我不必让我的代码正常工作,我可以更快地完成任务! - Jon Kruger
你说得对,我确实给这个点了踩。编写单元测试会限制你以面向对象的方式编写代码,以单位为基础,具有明确的依赖关系,并且可以独立运行。这意味着你的代码是可重用的,正确地传达了它所做的事情,并且不会对系统的其余部分施加限制。此外,你最终会测试你的代码,但手动运行整个应用程序而不是运行将一遍又一遍地为你执行此操作的代码,真是太可惜了。编写不可测试的代码总是使你的程序更难维护。 - Ben Barkay

5

在编写测试时(与任何其他软件任务一样),不要重复自己(DRY原则)。如果您有对多个测试有用的测试数据,则将其放置在两个测试都可以使用的地方。不要将代码复制到两个测试中。我知道这似乎很明显,但我经常看到这种情况发生。


2
大多数开发人员认为:“哦,这只是测试代码,如果它很糟糕又有什么关系?”这很令人遗憾,因为他们通常编写的代码已经够糟糕了! - dss539

4

我尽可能地采用测试驱动开发,这样我就没有任何不能进行单元测试的代码。除非有了单元测试,否则它不会存在。


1
绝对适用于数据库访问层。我不认为单元测试的概念适用于用户界面。 - John Saunders

4
最简单的方法是不要提交你的代码,除非你同时提交了测试。我不是测试驱动开发的忠实拥护者。但是我非常坚信一个观点,那就是必须连同测试一起提交代码。不是在一个小时或者在提交代码前几个小时的时候才补充测试,而是要同时提交。我认为编写测试和编写代码的顺序并不重要,只要它们一起出现即可。

1
@JaredPar:先写测试的原因是,当你检查测试时,你会知道它们测试的是你正在检查的代码。否则,你只能依赖于聪明的开发人员来确保测试实际上测试了有意义的东西。 - John Saunders
2
@John - 无论如何,你都需要依赖于杰出的开发人员。没有完美的流程可以将一个彻头彻尾的白痴变成天才。愚蠢总是能在流程中找到生存的方式。 - dss539
@John,我不同意。首先编写测试只能保证调用了哪些方法,而不能保证执行了什么代码。这是两个非常不同的问题,后者更加重要。我的团队不依赖于优秀的开发人员来确保代码得到适当的测试,我们依赖于性能分析工具来做出判断。 - JaredPar
@JaredPar:代码覆盖率是一个独立而有趣的事情。测试的目的不是为了测试代码本身,而是为了测试由测试调用的代码是否会导致正确的事情发生。我并不一定关心为实现正确的结果而调用哪些代码;因为我编写最少量的代码来通过测试,在开始时,很少有“额外”的代码不需要通过测试。 - John Saunders
@dss539:对于一个我不信任其专业性的开发人员,我会在接受测试之前审核所提议的测试列表和实际测试。这将捕捉到没有测试任何内容或测试自身等问题的测试。请注意,你不仅要先编写测试,还要编写失败的测试。只有通过创建代码使它们通过才能通过。如果遵循该过程,则会生成最小的通过编写的测试(最初失败但现在已通过最小代码编写)。也许这就是为什么要进行配对编程的原因? - John Saunders
显示剩余10条评论

4

小而高度内聚的方法。我是通过艰难的方式学到这一点的。想象一下,您有一个处理身份验证的公共方法。也许您已经做了TDD测试,但是如果该方法很大,那么调试就会很困难。相反,如果那个#authenticate方法以更伪代码的方式执行操作,调用其他小方法(可能是受保护的方法),当出现错误时,很容易为这些小方法编写新的测试并找到有问题的方法。


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