资深开发关于单元测试的5条建议

虽然人人都认为单元测试很有用,但在实际工作中,有完善单元测试的项目仍然凤毛麟角。大家拒绝写单元测试的理由总是千奇百怪:“项目工期太紧,没时间写测试了,先这么用吧!”“这模块太复杂了,根本没法写测试啊!”“我提交的这个模块太简单了,看上去就不可能有 bug,写单元测试干嘛?”

这些理由乍听上去都有道理,但其实都不对,它们代表了人们对单元测试的一些常见误解:

  • 工期紧没时间写测试:写单元测试看上去要多花费时间,但其实会在未来节约你的时间。
  • 模块复杂没法写测试:也许这正代表了你的代码设计有问题,需要调整。
  • 模块简单不需要测试:是否应该写单元测试和模块简单或复杂没有任何关系。

在长期编写单元测试的过程中,我总结了 5 个与单元测试有关的建议,希望它们能帮你更好的理解单元测试。

资深开发关于单元测试的5条建议

1、写单元测试不是浪费时间

对于从来没写过单元测试的人来说,他们对单元测试的看法往往是这样的:“写测试太浪费时间了,会降低我的开发效率。”从直觉上来看,这个说法似乎有一定道理,因为编写测试代码确实要花费额外的时间,如果不写测试,这个时间不就省出来了吗?

但真的是这样吗?不写测试真能节省时间?让我们看看下面这两个场景:

假设你在为某个博客项目开发一个新功能:支持在文章里插入图片。在花了一些时间写好功能代码后,由于这个项目没有任何单元测试,于是你在本地开发环境里,简单测试了一会,确认功能正常后就提交了改动。一天后,这个功能被发布到了线上。

但令人意外的是,功能发布以后,虽然文章里能正常插入图片,但系统后台却开始接到大量用户反馈:所有人都没法上传用户头像了。仔细一查后才发现,由于你开发新功能时,调整了图像模块的某个 API,而头像处理功能恰好使用了这个 API。因此,新功能最后弄坏了八竿子打不着的头像上传功能。

如果这个项目有单元测试的话,上面这种事儿根本就不会发生。当单元测试覆盖了项目的大部分功能以后,每当你对代码做出任何调整,只要跑一遍所有的单元测试,绝大多数问题都会浮出水面。许多隐蔽的 bug 根本不会被发布出去,因为单元测试会将它们扼杀在摇篮里。

因此,虽然不写单元测试看上去节约了一丁点时间,但有问题的代码上线后,你会花费更多的时间去定位、去处理这个 bug。缺少了单元测试的帮助,你需要耐心找到改动可能会影响到的每个模块,手动验证它们是否工作正常。所有这些事儿所花费的时间,足够你写好几十遍单元测试。

另一个单元测试能节约时间的场景,发生在项目需要重构时:

假设你要对某个模块做大规模的重构,那么,这个模块是否有单元测试,分别对应着两种天差地别的重构难度。对于没有任何单元测试的模块来说,重构是地狱难度。在这种环境下,每当你调整任何代码,你都必须仔细找到模块的每一个被引用处,小心翼翼的手动测试每一个场景。稍有不慎,重构就会引入新 Bug,好心就会办出坏事。

而在有着完善单元测试的模块里,重构是件轻松惬意的事情。在重构时,你可以按照任何你想要的方式,随意调整和优化旧代码。每次调整后,只要重新跑一遍测试用例,几秒钟之内你就能得到完善和准确的反馈。

所以,写单元测试不是浪费时间,也不会降低你的开发效率。你在单元测试上花费的那点时间,会在未来的日子里,为项目的所有参与者节约不计其数的时间。

2、不要总想着“补”测试

“先帮我 Review 下刚提交的这个 PR,功能已经全实现好了。单元测试我等会再补上来!”

在工作中,我常常会听到上面这句话。情况通常是,某人开发了一个或复杂或简单的功能,他在本地开发调试时,主要依靠手动测试,并没有同步编写功能的单元测试。但项目对单元测试又有要求。因此,为了尽早进入 Review 阶段,他决定把已实现的功能代码先提交上去,晚点再补上单元测试。

在上面的场景里,单元测试被当成了一种验证正确性的事后工具,对开发功能代码没有任何影响,因此,人们总是可以在完成开发后补测试。

但事实是,单元测试不光能验证程序的正确性,它还能极大的帮助你改进代码设计。但这种帮助有一个前提条件,那就是你必须在编写代码的同时,编写单元测试。当开发功能与编写测试同步进行时,你会来回切换自己的角色,分别作为代码的设计者和使用者,不断从代码里找出问题,调整设计。经历过多次调整与打磨后,你写出的代码会变得更好,更有扩展性。

但是,当你已经开发完功能,准备“补”单元测试时,你的心态和所处环境就已经完全不同了。假如这时,你在写单元测试时遇到了一些障碍,你会想尽各种办法来粗暴移除这些障碍,比如引入大量 Mock,或者只测好测的,不好测的干脆不测。在这种心态下,你最不想干的事,就是调整你的代码设计,让它变得更容易被测试。为什么?因为功能已经实现好了,再改来改去又得重新测,多麻烦呀!所以,不论最后的测试代码有多么别扭,只要能跑起来就好。

测试代码并不比普通代码地位低,选择事后补测试,你其实白白丢掉了用测试来驱动代码设计的机会。只有在编写代码时同步编写单元测试,你才能最大的享受到单元测试的好处。

我应该使用 TDD(测试驱动开发)吗?

TDD(测试驱动开发 Test-Driven Development 的首字母缩写)是由 Kent Beck 提出的一种软件开发方式。在 TDD 工作流下,要对软件做一个改动,你不会去直接修改代码,而是会先写出这个改动所需要的测试用例。

TDD 的大致工作流如下:

① 写测试用例(哪怕测试用例引用的模块根本不存在);
② 执行测试用例,让其失败;
③ 编写最简单的代码(此时只关心实现功能,不关心代码整洁度);
④ 执行测试用例,让测试通过;
⑤ 重构代码,删除重复,让代码变的更整洁;
⑥ 执行测试用例,验证重构;
⑦ 重复整个过程。

在我看来,TDD 是一套行之有效的工作方式,它很好的发挥出了单元测试驱动设计的能力,的确能帮助你写出更好的代码。

但在实际工作中,我其实很少宣称自己在实践 TDD。因为在开发时,我基本不会严格遵循上面的 TDD 标准流程。比如有时,我会直接跳过 TDD 的前两个步骤,不先写任何会失败的测试用例,直接就开始编写功能代码。

假如你从来没试过 TDD,我建议你可以了解一下 TDD 的基本概念,试着在项目中用 TDD 流程写几天代码。也许到最后,你会像我一样,并不会成为一名 TDD 的忠实信徒。但没准通过 TDD 的帮助,你能找到那个最适合你自己的开发流程。

3、难测试的代码就是烂代码

在为代码编写单元测试时,我们常常会遇到一些特别棘手的情况。

举个例子,当模块依赖了一个全局对象(global object)时,写单元测试就会变得很难。全局对象的基本特征,决定了它在内存中永远只会存在一份。而在编写单元测试时,为了测试代码在不同场景下的行为,我们一定会需要用到多份不同的全局对象。这时,全局对象的唯一性就会成为写测试最大的阻碍。

上一页12下一页


留言