你真的需要单元测试吗?

博主最近在接触一些Android单元测试方面的工作,发现自己并没有体会到大多数文章所宣传的“单元测试会带来工作效率的巨大提升”之类的诸多好处,于是本着批判与自我批判的精神对单元测试做了一番研究,以下言论仅代表个人观点,如果不足,欢迎指教。

单元测试不是用来找Bug的

当你看到网上诸多关于单元测试的赞美时,仔细看看你就会发现很多说的其实是TDD(Test-Driven Development,测试驱动开发),不幸的是大多数人并没有注意区分这两个概念。在Writing Great Unit Tests: Best and Worst Practices中,Steven Sanderson强烈表达了自己的观点:Unit testing is not about finding bugs。简单来说,当先写代码后写单元测试的时候,单元测试就成了一种发现Bug的手段,但作者根据其几十年的开发经验指出这种手段其实是十分低效的,因为即使每个功能模块都能正常工作,但是仍然不能保证模块之间、模块与用户环境之间能正确交互,而后者往往是Bug的主要来源。单元测试或许能找到一些Bug,但相比集成测试和系统测试就显得十分低效了。
既然如此,那么单元测试为何又备受追捧呢?在How Google Tests Software中,三位谷歌的专家介绍了谷歌的软件测试之道,总而言之就是谷歌会在开发之初设计好单元测试(其实是用代码表达需求),在开发中不断迭代以通过全部的测试(其实是完成全部需求),最终交付给测试人员的软件已经经过一轮测试,如果还有集成后的Bug,就可以交给专业的测试人员发现了。这是一种典型的敏捷开发,可以看到单元测试扮演更多的是驱动开发的角色。
作为技术标杆的谷歌已经全面引入了单元测试,那么我作为一个普通开发者为什么还要提出一番质疑呢?请看下一节。

单元测试的边际收益

在经济学领域,有一个著名的边际收益递减规律,指在投入生产要素后,每单位生产要素所能提供的产量增加发生递减(二阶导数为负)的现象。在本文讨论的场景中,投入产出如下(引自:软件开发过程中值不值得写单元测试? - voidint's blog):

成本(投入)

  • 编写单元测试用例所额外付出的时间,短期内会拖慢项目进度。

收益(产出)

  • 提升代码质量。监督开发人员写出更加易于测试和可维护的代码。
  • 提升开发团队内部的协作效率。其他开发人员可以通过阅读单元测试用例来理解代码原作者的意图。
  • 保证功能实现的长期稳定。代码一旦发生与原功能意图不相符的变化,通过跑单元测试可以体现出来,即可以防止功能被无意识地破坏。
  • 提高自动化测试占比,降低其他测试方式上的投入。

在经济学中,边际收益递减现象常出现于产量的短期分析中。结合对同事的咨询以及自己的调研,这个现象在软件开发领域同样适用。当我们需要写原型或者开发一个短期紧急需求的时候,(产品、运营人员)往往要求快速交付,并且由于代码规模有限也往往不会有太多Bug,在这种短期开发中如果引入单元测试往往会适得其反,投入了双倍的时间却没有明显的附加收益。而分析How Google Tests Software一书中最多提及的几个项目(Chrome,Android,Gmail)可以发现,单元测试(更准确说是Test-Driven Development)的成功案例往往都是一些架构设计良好,处于长期迭代开发,基本没有短期临时紧急需求的产品,项目初期的单元测试往往在几年后还能使用,复用率极高(私以为复用率某种程度上可以作为是否值得引入单元测试的标准)。而如果一个项目一开始没有引入单元测试、过时和糟糕的代码没有及时重构、临时短期需求偏多,往往就没有引入单元测试的必要了。

Jake Wharton也头疼的单元测试

Jake Wharton何许人也?答:诸多著名开源项目的作者,Android社区的旗帜人物:
[图片上传失败...(image-547317-1523347911909)]
Jake Wharton对于Android平台的单元测试也十分头痛(Against Android Unit Tests),其原因也是我调研并写下本文的原因。Android相对于其他开发环境有以下几个特点:

  1. 大多数代码是在与Android环境(SDK)交互,逻辑往往放在后端
  2. UI与逻辑容易耦合,虽然MVP等模式的出现在尝试缓解这个问题
  3. 版本众多(不同ROM),使用环境复杂(用户行为、网络波动等)导致具体环境的Bug远多于单个模块的Bug

可以想象,当你投入大量精力,使用RobolectricMockito等框架模拟出一个将数据库数据发往后台的单元测试并通过测试用例后,用户却因为切换网络等小概率场景触发了Bug,你会不会感叹我要这单测有何用?类似Android这种终端环境,其边际收益递减的临界点往往更容易达到,引入单元测试犹需谨慎。

你的代码能做单元测试吗

结合上面的分析,哪些场景不适合做单元测试已经显而易见了,When is unit testing inappropriate or unnecessary? [duplicate]中一个高票回答做了如下总结:

  1. The code has no branches is trivial. A getter that returns 0 doesn't need to be tested, and changes will be covered by tests for its consumers.
  1. The code simply passes through into a stable API. I'll assume that the standard library works properly.
  2. The code needs to interact with other deployed systems; then an integration test is called for.
  3. If the test of success/fail is something that is so difficult to quantify as to not be reliably measurable, such as steganography being unnoticeable to humans.
  4. If the test itself is an order of magnitude more difficult to write than the code.
  5. If the code is throw-away or placeholder code. If there's any doubt, test.

Definition of brittle unit tests中也有详细总结,都有一定参考价值。
此外,有了适合单元测试的场景并不代表就有适合单元测试的代码。在TDD模式中,测试先于开发,所以开发部分的代码接口往往需要经过良好的设计和定义,最好能解耦各个模块,如此开发代码将能够完美匹配测试代码。但这种开发模式往往对开发经验、设计能力要求很高。能都达成此境界的已经是TDD的行家了。然而事实是对于没有单元测试经验的开发人员而言,往往没有意识到自己写的代码“不可测试”。以下面伪代码为例:

object processObject(Object object) {
    if (object == objectA) {
        log.i('error 1 ....')
        return object;
    }
    if (object == objectB) {
        log.i('error 1 ....')
        return object;
    }
    .....
    return object;
}

开发人员在Debug的时候,能根据log信息快速定位问题,但对于测试来说就十分不友好了:返回值都一样。如果想要领会单元测试的优越性,短期的镇痛与适应似乎是不可避免的。

写在最后

本文没有讨论TDD的各种优势,也没有讨论单元测试的最佳实践,是个人的一些总结,讨论的是单元测试的一些局限之处,或许有不足、有遗漏,又或者完全错误,欢迎拍砖。譬如,在stackoverflow上有一个关于是否值得做单元测试的问题就因为其争议性而被关闭回答,而又因为有其存在的历史意义而被一直锁定(locked),感兴趣可以看看:

[图片上传失败...(image-8d7ff-1523347911910)]
链接:Is Unit Testing worth the effort? - Stack Overflow

本文讨论的只是单元测试(TDD)的局限性,在合适的场景中其作用是巨大的,尤其是轮子级、框架级和开源项目中,了解单元测试也大有裨益。比如,我在为ChatteBot提交代码时,就因为当时不了解单元测试的作用,只修改了代码Bug而没有修改测试代码的错误(测试代码写错真是没救了)。我也因此失去了成为一个5000+ star开源项目贡献者的机会.....
[图片上传失败...(image-655ca6-1523347911910)]

参考

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,451评论 25 707
  • 用到的组件 1、通过CocoaPods安装 2、第三方类库安装 3、第三方服务 友盟社会化分享组件 友盟用户反馈 ...
    SunnyLeong阅读 14,599评论 1 180
  • 面对恶行,你是缄默还是反抗? 哪怕与你无关。 文明失守,你是据理力争还是拱手让人? 哪怕势单力薄。 是“包容”,还...
    观察员yog阅读 896评论 12 13
  • 我本平凡,却长了一身“娇气” 让我不得不戒,戒,戒 让我不得不忌,忌,忌 受不了,受够了 抱怨,埋怨…可是有用吗?...
    Turbo六耳悟空阅读 301评论 0 0
  • 文/陌宇轩 这个菜园子和其他的园子有所不同,老种着各种果树,实验田,两位老人呵护这片土地。几十年,土地翻了又...
    小哲小诗阅读 274评论 0 0