你好,单元测试!

^

平时面试程序员,如果想起来,经常会问对方2个问题:

  • 平时写代码会进行单元测试不?能聊的话,就再跟对方交流下细节。
  • 你们公司或所在的团队进行研发过程中有单元测试不?然后也是能聊的话,就再跟对方交流下细节。

其实这2个问题主要出于2个目的:

  • 个人习惯,喜欢收集统计下市面上各个IT公司的研发阶段的单元测试情况。
  • 如果面试者有单元测试习惯,当然有总比没有好嘛。

回想一下

相信有一定软件研发经历的程序员,对于以下场景或言论应该经常碰到:

  • 坑爹了,还有一周就要交付版本了,还有好多功能还没做完呢,做完的还有好多bug呢。。写单元测试?开玩笑吧 – 一个苦逼加班的程序员。
  • 擦嘞,怎么这个bug又出现了。。 – Mr.救火员经常说的话。
  • 纠结啊,代码好乱,好想重构,但万一该出问题了咋办。。算了就这样堆代码吧 – 第n个维护该代码的程序员的心理活动。
  • 你妹啊,你们开发自己不测试吗,这么多问题根本没法用嘛 – 一个很爷们的测试姑娘发怒了。
  • 你说说,为啥发布版本周期越来越长,bug反而还越来越多了呢?! – 某个高层管理者的疑惑。
  • 还有更多日常用语、抱怨、吐槽,欢迎补充。。

当然,导致这些吐槽的原因必然不是没进行单元测试导致的,这里只是为了说明,如果有单元测试的话,对于上面的场景在一定程度上能有所避免。

你怎么看?

不知道各位程序员是如何看待单元测试这个问题的?经常会有类似说法吧:

  • 我太忙了!
  • 我没时间啊!类似上面说法。
  • 我认为功能代码更重要。
  • 测试代码软件中实际不会跑。
  • 我写代码时候很仔细,边写边手工执行下测试。这其实也不错,这个执行过程其实从广义上讲,也是单元测试,但是可重复程度较低而已(比如哪天我偷懒,即时那个程序员不偷懒,但换了个程序员维护,你能确保第二个程序员不偷懒?好,即使第二个程序员不偷懒,那么第n个程序员呢?)

其实,每个程序员对于单元测试都会有自己的看法吧。就我的观点而言,单元测试是很必要,不然也不会闲的写这篇文章吧。

不是什么

说是什么之前,先说单元测试不是什么吧:

  • NOT 万能药
  • NOT 老鼠药

一种观点认为,这是个很牛X的东西,有了它,软件质量必然会好。这就是很多人觉得有了单元测试保障以后,就万事大吉了,这个想法很危险,说到底任何一种实践仅仅只是程序员工具箱中的一种工具而已,即使是所谓万能钥匙,它也仅仅能用来开锁、开门不是吗(如果你非得想出拿钥匙来抠耳屎,好吧,这也算一种功能)。再好的实践,再厉害的工具,也能让一个不合格的程序员用烂了。

另一种保守的观点认为,这个东西不好,为什么不好?无非就浪费时间,增加工作负担,很繁琐这几种理由。但的确是这样吗?很遗憾,是的,不过得换种说法:会花费原来你没花费的时间,提升你的工作质量,是否繁琐是一个习惯成自然的问题。说白了,这里其实是一个熟练度的问题。

是什么

再来说下单元测试是什么吧:

  • 润滑剂
  • 催化剂

好吧,我承认为了对应上面的“不是什么”故意类比出2个是什么,其实可以认为是一类东西,也就是对于保障软件质量来说,单元测试是让软件实际的功能代码能,更正确的运作(润滑剂),更好的进化(催化剂)。也就是更好的校验你编写代码的输入输出预期,重构改进代码时候能更大胆的对现有代码动刀子。

单元测试代码

需要指出的是,这里的单元测试是泛指,也就是至少你写完一段代码或改完一段代码,这段代码运行的主路径或代码被修改的地方要运行一遍,也就是传统的跑一遍程序。当然这个是必须的,因为你得确认程序是按你预期的输入和输出来执行的。

但这种形式的单元测试有个缺点就是可重复性太低,这里“重复性”的意思就是,任何人想任何时间可以随时跑一遍程序,等于跑这种形式的单元测试,太依赖于人,谁都有想偷懒的时候对吧。

也就是:

  • 人工跑一边函数、模块、功能,也是单元测试。
  • 但人总会偷懒(我很忙、我没时间、我病了、我状态不好、必杀绝招:我宣布这坨代码不归我管了。。),造成重复性太低。

所以更理想的单元测试方式,还是颤抖吧,把你的测试过程写成代码吧,交给机器去跑单元测试。也许你人工跑一边程序只要1秒钟,但写测试代码可能耗费了你10分钟,貌似是600倍的耗费,但你写的代码的生命周期值得拥有这600倍的耗费,代码可能被频繁改动,每改动一次,都应该测试一次(估计很多程序员都是以自己的人品保证,自己的修改是不会有问题的,从而忽略了这一次测试吧),并且可能维护代码的不是你,这么看,600倍其实不多,对吧。

功能代码 + 测试代码

这里的测试代码特指单元测试代码。

  • 互相验证
    • 其实对于功能代码和测试代码而言,两者是一个互相验证的关系,也就是“好基友,一辈子+一被子”的关系。
    • 经常有人会问,测试代码不也是代码嘛,那谁来测试啊,答案就是功能代码,不至于出现“测试测试代码”之一说法吧,然后无限递归下去吧。这个疑问的担心是,代码是人写的,难免会出问题,测试代码也是代码,所以自然也难免出问题。
    • 其实这个担心是对的,消除这个担心,一个是借助成熟的单元测试框架,减少各种细枝末节的考虑,只关注测试验证,必然能减少很多犯错,但这只能减少,想要规避是不可能的,但可以事后补救,比如测试代码的错误,导致不应该测试通过的,竟然测试通过了,那么这个时候功能代码必然也出错,这个时候不管有没有发现,就必然是产生问题了,如果被发现了,那么就知错就改呗,找到绕过测试代码的地方,看看怎么修改下测试代码逻辑,是否能测试到这种情况。
  • 先写?后写?
    • 有人经常的疑惑就是,我到底是先写测试代码呢?还是写完功能代码再写测试代码呢?这个不好说,感觉看个人习惯吧,我的习惯一般都会是偏向后写,因为很多时候在功能代码没出来前,有些测试代码的测试用例设计很难。
    • 当然一些通用、普遍或者业务逻辑简单的场景写还是可以尝试下所谓“测试先行”的做法的。
    • 这里说的前提,貌似就是由程序员自己负责来写对应的测试代码了。可能有的公司会区分出,开发工程师和测试开发工程师,前者更偏重功能实现代码为主,测试代码为辅;后者更偏重测试代码为主。这个时候,先后也许不那么重要。
  • 度的问题
    • 还有人的疑惑就是,到底写多少测试代码才算到头啊?我的想法是,别想了,永远到不了头的。合理的做法是够用就行。
    • 比如用户注册,最起码测试下注册成功的情况吧,然后测试失败的情况吧,当然失败情况一堆,咋办?挑个主流的失败场景呗。
    • 如果那些边边角角测试不到咋办?别急着一口气吃撑胖子,慢慢补呗,毕竟你主要任务还是实现那个注册功能,测试也会服务于这一点的。但如果你非得一天把罗马城建完,那我也无话可说,有钱、有时间的人就是牛X。
    • 其实我这说了也白说,度的问题,往往是自己慢慢体验的。

测试代码只是零散的测试片段,要把这些测试集合起来,必然还是要配合测试框架、工具自动化之类的。还是那句话,机器不会偷懒。

场景

说了这么多,来说个简单的单元测试应用场景吧:

  1. 天哪!我写的代码竟然有一个bug!
  2. 吭哧吭哧,调式了半天,终于找到了触发条件,一个很诡异的条件。
  3. 咚咚!打断一下,切换到传统场景,这个时候一般就继续吭哧吭哧修复功能代码了,然后提交修复,perfect!感觉很有成就感。
  4. 叮叮!忽略刚才的打断,goto到2。这么诡异的条件,太容易被忽略了,还是加段测试代码保险一下吧,免得别人修改代码时候也碰到这种坑爹的情况。
  5. 吭哧吭哧写了个单元测试代码,来模拟刚才的条件触发,果然测试不通过!
  6. 吭哧吭哧修复完功能代码。
  7. 执行单元测试模拟条件触发,测试通过,看来修复还是有效的。

其实这个好处就是能以后避免出现这种条件触发的bug被再次引入,至少有了个保障。

但是:

  • 以后不再触发该bug!
  • 了吗?不一定噢,说不定有个更诡异的条件也能触发该bug呢。。
  • 那为什么还要用?其实前面也说了,至少封堵了一个诡异条件了,如果又发现其它诡异条件,那么继续goto到1,周而复始。

示例

实际点,用代码说话才是王道。随便找几个开源项目的代码提交,来说明下单元测试的是怎么做的吧:

其实这里体现了一个软件配置管理(SCM)的深刻理解,就是: 一切开发行为,统一在版本仓库中进行自动记录。

也就是传统的手工跑一边功能代码的单元测试这个测试行为没有被记录在版本仓库中,而单元测试代码则很好的帮助记录了,这里2个示例更好的还有对应文档的更新。

这仅仅是开始

单元测试其实只是众多环节的一小部分,或者说是一个起点吧,按顺序演变:

  • 测试框架。有了成熟的框架,必然事半功倍。
  • 集成测试。其实也是借助各种成熟工具,来帮你做代码层面的集成测试吧。
  • 自动测试。机器帮你跑测试了,自然就自动了。
  • 持续构建。每次提交代码构建也好,每日构建也好,跑一遍单元测试一般都是很重要的一个步骤吧。
  • 持续发布/交付。前面基础打得好,自动化程度足够高,这种层面的持续就不是梦想了。
  • 高效高质量迭代。注意不仅仅是高效,而且是高质量。
  • 质量可靠的软件、服务、项目、产品。其实这个才是我们的目的不是吗?单元测试只不过是达到我们目的的工具箱中的众多工具之一。

$

结束语就用这句话吧: one test a day, keep bugs away..

当然这必然是夸张了,而且有些反了,有时候经常是来了个bug,才多了个test的;-)

后续

这篇文章目的,只是希望大家能对单元测试有个感性的认识,也就是:

  • 原来不知道单元测试的,能记住单元测试这个名词。
  • 原来听过的,能认可单元测试这种形式。
  • 认可了的,能产生去实践单元测试的欲望。
  • 有了欲望的,能尝试去实践一把。
  • 已经在实践的,那么就把你的实践经验、教训、总结或吐槽各种分享出来。

后续将从实际操作层面来讲解,比如以Python为例、以JavaScript为例等等。

注解

这篇是个人总结的《软件构建实践》系列的一篇文章,更多更新内容,可以直接在线查看:http://pm.readthedocs.org。并且部分内容已经公布在GitHub上:https://github.com/akun/pm