我曾经是一个不测试主义者,因为我看不到测试的价值。然后,我试了一段时间,变得对它深信不疑。我收集了一些经验,当然还远远不够。这篇文章总结了一些我知道的以及我认为我知道的内容。
本文的灵感主要来自于《JavaScript. Air episode 004》,但这里也有一些原创的内容。并且有的来自《TDD: Where did it all go wrong?》。
我不总测试我的代码,但是当我测试的时候,感觉更好。 —— 我 |
这是怎么一回事呢?
这,全是因为代码:
本文主要关于单元测试,而不是集成测试或端至端的测试,但在某些方面也可用于其他测试。在实践中,测试很少是单一的或非此即彼的,并且这也不是目的。
单元测试成本低廉,因此应该成为测试工作中最大的组成部分。编写和运行单元测试都很便宜。因为它只查看代码的特定部分。集成测试则相反,它们包含的代码更大。
为什么这很重要?
测试可帮助你对你的代码放心。对一个稍复杂的问题写一个解决方案,然后手动测试,你只需要这么做就可以了。有着一定经验的你当然可以自信地发布代码,但是结果却往往是抛弃了发现错误的第一次机会。
测试能让你体验你的代码中在最极端的条件下是什么样的。要是传递的数字是负数,会怎么样,在我们总是假定数值为正的情况下?要是传递的根本就不是数字,会怎么样?
每个人都会写出 bug,我们都写过 bug。因此,这不是“你能正确地编写代码或一次性写出正确代码?”的问题,我们都写过不正确的代码。这就是我们所做的一切,我们写的都是不正确的代码。 ——Joe Eames《JavaScript. Air 004》 |
编码是辛苦,我们都应该承认这一点。其中的主要原因之一就是,你需要测试代码,以获得它能如期表现的信心,不管是什么代码。
不相信?这里给出了一些附加参数:
测试过的代码更好
许多人会告诉你,代码测试会导致更好的代码质量。这在使用单元测试,并且至少在测试驱动开发上有所行动,即使这些行动甚为草率时,尤其如此。原因如下:
如果你的代码难以测试,那么可能是你代码没有写好。好代码的定义是什么,这是一个大问题,但这里要强调的一句话是一个很好的经验法则,也是大多数人所赞同的,那就是,好的代码会分离关注点。有经验的程序员限制功能体以便于只做一件逻辑上的事情就是这个原因。
目标对齐
代码很难测试可能要么是因为有太多的事情要继续,要么是因为有太多的依赖(或两者皆有)。考虑将此视为协调利益的一个问题:在编写未经测试的代码时,在速度(或懒惰)和关注点分离之间存在着利益冲突,并且短期内你的代码是如何被组织的并没有那么重要。当代码必须测试时,你的目标更一致,因为对于写得好的代码,更易于写测试!
像消费者一样思考
当你第一次编写测试时,你首先要设计代码的 API。测试让你进入代码消费模式,在这种模式下,你的代码需要面对其他东西的接口。设计 API,而不那么关注内部运作将导致一个更佳的 API 设计,这会导致模块的更易消耗,从而促进项目代码的更干净。
灵感突现
测试会让你灵机一现。通常情况下,因为它迫使你去思考边缘情况——零值,10 ^ 12,null 或 undefined。这使得你有机会来思考。去反省,以及了解在陌生的环境下会发生什么。仅仅是思考这些的过程,或代码将面对的其他情况,都经常会让你意识到代码可以简化(以及代码需要如何保护自我)。
这些灵感突现的时刻也可能来自最令人沮丧的情况之一:当你的代码和测试不一致的时候。你正处于不知道哪个才正确的两难境地。如果你碰到这种情况,那么设计可能有问题,或者你的前提假设发生了变化。把它看成是一个好兆头!你的代码将会更满意。
测试可以说明代码做了什么
没有人喜欢写文档,但当你继承(从一年前的自己,或其他人)或接口的模块文档齐全的时候,绝对是好的。测试可以成为这样一种途径,并且还有一个额外的好处是:测试用实际行动证实代码。就如同最佳的科学教师,他们不只是用嘴巴告诉你,氢气易燃,而是充了一个氢气球,让它升到天花板上,然后在棍子上放一根点燃的火柴靠近气球(这是我五年级时最难忘的时刻之一)。
你知道所有 bug 的共同点吗?那就是它们通过了所有的测试。所以,当你找到一个 bug 的时候,就等于知道测试哪里还需要改进。
测试可以使得更容易地加入项目,因为它们揭示了代码实际上应该做什么。它们告诉你设计决策,以及初始的开发人员心里在想什么。
不要担心,去重构吧
也曾看到过乌七八糟的代码,但不敢去清理干净?我在这种情况下要做的第一件事是创建测试来找出代码要做什么。测试可以锁定功能,用一种很好的方式,使得我们能够专注于“大扫除”,而不是担心破坏什么东西。
我见过一些糟糕到让人不知道它们是做什么的代码片段。同样的,人人避之唯恐不及,不但要担心会破坏预期的功能,而且还要担心破坏 bug。我认为基于过去的I/ O 的大型测试集是非常值得的投资。
有趣的是,担心和快乐的心情是成反比的。总之是一种此消彼长的状态。
自信地创造价值和正确的产品
正确的代码比不正确的代码更有价值。一切帮助你的代码比以前更正确的东西都值得看一看,就这么简单。发布正确的代码随着时间的推移会构建起信任,而信任是一笔宝贵的财富。
鱼与熊掌不可得兼
这里有一个技巧:不要在试图解决问题的同时,设计一个很好的解决方案。来自于 Ian Cooper 关于 TDD 演讲中的秘诀是:
编写红色测试。
解个问题,尽快让它变绿。
设计一个很好的解决方案,重构成你为之骄傲的一个东西。
这里要掌握的一个重要内容是,在你的大脑中要分离关注点。不要试图同时完成步骤 2 和步骤3。编程的主要限制之一是你的大脑一次能思考多少,并且在你敲代码时,你需要思考得越少,你写的代码越好。
在解决问题时,不要去想代码实际上应该如何。复制粘贴代码,写低效的循环,重复内容,不论是什么只要能尽快让测试变绿就去做。然后再考虑如何改进。
分离关注点是首先要测试的原因之一,这种方法有助于实践中行为。当你不择手段地想要快速达成一个解决方案时,你不必去考虑它看上去怎么样或者运行起来快不快。当你进行到完善设计和改善解决方案的时候,你就不必担心解决方法行不通了。
知道测试什么是关键
知道测试什么没有听上去得那么容易,并且有很大一部分是由经验所决定的。许多测试测试得太多。知道要测试什么涉及到要了解什么重要,什么不重要,而要知道这些并不是一件随随便便就能做到的事情。这里有一个技巧,但:
尽可能采用最高级别的测试,以便于在实现上覆盖范围和灵活性。 ——Brian Lonsdorf,《JavaScript. Air 004》 |
所以,基本上:
不要测试内部的东西,这只会成为你的阻碍。如果你真的觉得你应该测试内部的东西,那么你最好分离成一个新的模块,使之成为外部的东西。
不要测试过于指定,或处理它们不必和不应该知道的东西。
不要只是为了获得 100% 的覆盖率而去写测试。如果有人告诉你应该保持 100% 的覆盖率,那么不要废话,揍他。
请记住,测试应该从模块外部的角度开始由外到内。需要注意的是完全覆盖的测试还是有可能的,即代码的所有分支应该都可以实现。如果没有,那么它们基本上是死码,不是吗?除非你需要更好地理解它们是如何工作的,否则就不要测试内部的东西。
想想当一段时间以后,代码重构的时候,会发生什么。实现应该允许在测试不失败的情况下被更改。为什么?因为如果将来的程序员需要改测试的话,那么基本上是重写,而不是重构。并且重写并不安全。对于重构内部应该没有新的测试。
在测试时要务实。测试是项目以及创造价值的一部分,什么都拿来测试没有任何意义,就像实现所有按钮没有意义一样。记住文档方面。如果测试涉及许多实施细节,那么我们就会失去模块的重点。我们就会失去文档的价值。
至于文档,测试你的领域假设。这些都是你工作的问题域的代码解释,这些问题域往往是一些程序员不擅长的地方。用代码的形式文档化这些假设解决了两个问题:自我文档化假设,并证明它们能够如解释那样有效工作。
当你发现 bug 的时候,编写测试。不要只是修复它。去写测试,确保它既是红的,又对齐 bug 所没有意识到的期望。修复 bug,使其呈现绿色。保存。
代码覆盖作为一个具体的数字被高估了,但作为一种工具它还是很有用的。不要为了覆盖范围而力求覆盖。请记住,覆盖范围只能告诉你测试在代码行运行什么,而不会告诉你测试将运行什么组合。不过,这可以成为事情是否朝着正确方向前进的一个很好的风向标。如果重构导致更糟的代码覆盖范围,那么就应该响起警铃,尤其是如果它是重构的话。不要只是为了增加覆盖数值就让自己去编写测试。经过充分测试和编写良好的代码的覆盖数值更大。
编写测试的触发器是当你的代码片段有新的行为的时候。测试应该盯牢这种行为,但不要矫枉过正。
测试库可能比测试终端应用程序更容易,更为重要。毕竟,库会被多个应用程序使用。
如何编写特别棒的测试
知道如何写出好的测试是关键,因为很容易写得不好。事实是,和其他所有一切一样,它需要实践。不过,这里有一些小贴士。
好的测试往往是简单的。它不会尝试一气呵成面面俱到。它的名字反映了它要的目的,并且名称应该精简成一句话。例如,名称不应该是“it works”,而是“it returns 0 for negative values”。
hello
确保测试不要过于指定。 过于指定的测试涉及到太多内部东西,并且不允许重构。
单元测试运行代码时会隔离其他测试,不一定是其他代码的测试。它将代码带出它的上下文,并创建其中一个方面的人工上下文,以便于进行调查。然而,这并不意味着单元测试必须得在隔离其他所有代码的情况下运行,尽管这通常被认为是“纯单元测试”。所有一切都没有必要 mock 和 stub,因为只会导致更复杂的设置,更低的覆盖率和更加脆弱的测试。
在有意义的地方使用 mock 和 stub。你不想对一个真正的 HTTP API 进行测试,那就 stub。如果你正在测试的东西是你自己对该对象的调用,或你想要自己的代码历经某个路径,那么使用使用 mock 和 stub。
测试读起来应该像一个小故事,遵循 AAA 体系: Arrange、Act、Assert。设置东西,做出声明,并且断言声明做了它应该做的。 “小故事”方面要重视小的方面。“3A”中没有一个应该超过 3 行代码以上。在阶段之间留一些空间会更好。应该没有任何分支和循环,你在断言时应该只涉及一个逻辑内容。 (如果一个断言语句就能表达自然是好,但有时你需要更多,那也没关系。)永远不要在测试的两个不同的地方断言,因为这会导致你实际测试的混乱。
测试应该只需要一些领域知识就可读。如果不深入模块的内部运作就很难解释的话,那么要么你最好多花一些时间在测试上,那么彻底弃之不顾。
一般情况下,不要测试依赖。对于某些项目,对一些代码所做的假设做一些简单的测试,可能是有意义的,但要谨慎和小心。测试库是库作者的工作。相反,要依靠更新日志进行升级,以及依赖于测试集成而不是库(不用 mock 一切的一个原因)。
编写不需要很长时间运行的低成本测试,因为要时常运行这些测试。如果你可以传递 --watch 参数到你的测试运行中,并且在每次有文件改变时运行它,那么这是一件好事。
最后但并非最不重要的一点是,使用你喜欢的测试框架。如果 JavaScript. 是你的菜,那么我会推荐 AVA,因为它清晰简单,而且没有复杂的配置。不管你选择什么,确保测试框架能和你一起工作,并帮助你编写测试更高效,更快捷。正如编码一样,如果你觉得不好玩,那么可能有什么地方出错了。