单元测试不是持续集成的基础 - fsword.github.io

关于软件测试,最近也在持续实践,对这篇《单元测试不是持续集成的基础》感同身受,作者李福,来自阿里技术质量部。

在我司实践单元测试中,有的单元测试刚写好,业务一发生变化就不得不完全抛弃,耽误时间。当然也有一种说法,单元测试代码的稳定是主要靠好的API设计:如何保持unit test代码的稳定。“API切实正确切割了需求,那么在重构的时候API就基本不用变化,unit test也不用重写。”关于这个我也是赞同的,然而说法过于理想化,难以实践落地,毕竟“好的API设计”这事还是依赖于人,可能第一个人可以,后边换了一个人设计API,整个项目就乌烟瘴气了。

说个题外话,也是因为成本的原因,个人比较倾向于BDD而非TDD。对于研发测试目前有这两点心得:

  • 测试的出发点不是找漏洞,而是对设计任务的验证
  • 测试的依据是文档,可执行的文档才是好文档

原文

很多人关注甚至想尝试持续集成,然而也有一些人担心团队缺乏基础——“我们连单元测试都做不好,做持续集成不太合适吧”。如果不会走就学跑,那确实容易摔跤。不过,单元测试和持续集成并不是走和跑的关系。

为避免误解,首先明确一下名词(虽然没有照抄书本,但是应该不会差太远吧):

  • 单元测试:以验证某个代码单元的正确性为目标进行的自动化测试活动,“代码单元”通常是函数、方法或者是类,测试过程中,目标单元对外部的编译或者功能依赖由stub或者mock技术进行隔离。
  • 持续集成:一种敏捷实践,重点是尽早进行系统的集成测试,它在狭义上包括部署自动化和测试自动化。“持续”一般被理解为不断的对研发变更进行整体验证,“集成”通常包括对分支的集成(因G此一般推荐单分支开发)和在一定条件下对不同子系统或者模块进行的集成。 可以看出,单元测试针对的目标是局部而非整体,而持续集成面对的是整体。按照“饭要一口一口吃”的老话,似乎应该先做单元测试。

然而单元测试并不是免费的。任何自动化测试都是基于测试目标的功能而实现,因此,测试目标的稳定性就变成了测试价值的一个重要因素——时常发生变化的代码,对其做自动化测试是不划算的。那么,单元测试所针对那些小粒度的类、方法和函数,它们变化剧烈吗?

这可能和软件系统的类型有关。例如,如果我们是在开发一个短信发送客户端,由于所遵循的SMGP协议本身是相对稳定的,网络相关的功能单元就是稳定的;然而,如果我们开发一个应用系统(比如各种大大小小的互联网应用),业务上的变化可能对下层的模型代码产生天翻覆地的影响。考虑到大部分的软件研发团队和研发工程师们所处理的都是基于数据库+web的应用系统,我们所遇到的场景很可能是后一种情况。

去年我所在的团队推进质量改进时我们就发现了这个规律,当时我们首先推进的就是单元测试,虽然我强调“自动化测试”而非单元测试,但是开发同事们都很自然把精力放到了单元测试上。在一段时间的热心实施以后,一些人开始出现不同的声音——“有些测试刚写好,业务就发生了变化,不得不完全抛弃,瞎耽误时间”。问题显然不是同事们不尽责,我们分析发现,因为单元测试用例过于关注细节,业务变化的情况下很难进行积累,再继续下去会出现“边际效益递减”的情况,而如果开始做持续集成方面的工作,则可以补充自动化的集成用例——它相对稳定。

除此之外,单元测试还有一个常见的问题:mock的代价。

几年前ThoughtWorks的李晓有过一篇《不要把Mock当作你的设计利器》这里还有gigix转述郭晓的观点——

I did have some doubts about using Mocks when i was programming, similar 
reasons - too hard to refactory, too brittle. And i total agree with the 
three places to use it - external resources (I/O), UI, third party API.

也许有人觉得这里的观点有些“极端”(好像中国人对“极端”是比较敏感的 :-D ),然而我们在实际工作中很容易感受到上述文章和引论所说的痛点。这里存在两个方面的问题——

  • 对变化不友好:一旦我们进行了mock,就在事实上建立了对外部变化的“屏障”——每次发生变化时都有可能忘记了被mock掉的“结合点”,即使记得,也增加了重构的成本,时间一长,维护mock代码就变成了一件苦差事
  • 推迟集成:有了mock以后,我们可以很容易就建立起自动化验证机制。但是错误往往在于疏忽——mock掉的那个东西,未来需要使用“真实的东西“再测一遍,这不止增加了测试成本,而且还会在前期给人以“系统没问题”的错觉 顺便说一句,这些问题在stub中也是类似的,mock和stub还有一些差异,但是这里就不涉及了。

对于这些分析,路宁同学 的一个简单易用的观点是:“不对自己开发的模块写mock”,这个很好理解,因为自己开发的模块可以直接用“真家伙”,那么“假李鬼”也就用不着了。

我们是否可以沿着这个思路继续推进呢?实际上,之所以要区分“自己的模块”,是因为“自己的模块”好合作(自己和自己当然好合作),那么在我们推进持续集成以后呢?

持续集成,表面上看是在做部署自动化和测试自动化。然而这个实践的一个重要价值是“弥合缺口”——通过持续的将版本控制系统的多个分支合并到一个分支上,避免了分之间的鸿沟越来越大;通过持续的将系统的各个部分完整的部署在一起进行自动化联调和系统测试,避免了子系统与子系统、模块与模块之间的衔接隔阂。

前者好理解,后者一般容易被忽视,我们知道,某些语言特性和框架也试图解决这种系统和模块边界 的衔接问题,例如java的interface,它就是设计来建立系统间协作接口的。然而真实的世界很难用 interface这类技术进行约束,即使实现了同样的interface,我们也不确定边界两边都遵守共同的约 束,能让我们放心的只有联调和系统测试。 显然,在我们推进持续集成的工作并通过这个工作不断的“弥合缺口”以后,那些之前不得不mock掉的所谓“别人的模块”甚至“外部的子系统”也就不再变得遥不可及而难以合作了。于是,我们惊喜的发现——mock变得可以省略了,随着持续集成的推进,一些原来不得不编写的mock可以直接用“真家伙”代替,而原来所倚仗的单元测试用例也随之变成了集成用例、联调用例……

所以,单元测试并不是集成测试的基础。实际上,往往是持续集成扩展了质量保障的手段和方式,并因此减弱了单元测试的压力,从此我们可以专注在必要的单元测试用例上了。

如果你的团队做单元测试不是很给力,可以先找找原因,如果不是大家的主观意愿问题,不妨和持续集成的工作一起推进吧

参考资料


Laravel 中 Lang get 方法的简单实现 Chrome 插件 - Laravel TestTools