《黄帝内经》:“上医治未病,中医治欲病,下医治已病”,意思是医术最高明的医生并不是擅长治病的人,而是能够预防疾病的人。而单测,就是上医。
本文从三个方面讲解:
走近单元测试
为何要单元测试
如何进行单元测试
走近单元测试
我们先说测试,测试的本质就是在我们生产代码写完之后,上线之前,要对这个代码的正确性进行验证的一个工作,那么单元测试就是整个自动化过程之中,最基本的测试、覆盖率最多的一种测试类型。Mike Cohn在他的著作《Succeeding with Agile》(敏捷软件开发)中提出了测试金字塔,底层是单元测试,中间层是 集成 测试,上层 是UI 自动化测试。
而且底层的单元测试需要做最多的测试工作,越往上测试工作应该越少。根据《谷歌软件测试之道》的经验,三者对于精力投入的比例是:把 70%的精力放在单元测试,20%放在 集成 测试,而剩下 10%的精力放在 UI 测试。
上图从速度以及成本方面,很形象的表明三种测试之间的区别,明显可以看出单元测试是成本最少,效率最高的一种测试策略,也是更快得到反馈的一种策略。
单测,不是那么容易的
很多开发,从接到需求开始,一顿刷刷刷代码输出,然后交付给测试,结果出来N个bug,然后不断的改bug,再回归,看着每天忙的飞起,卷的要命。先不说浪费测试和开发资源,但可以预见,随着需求的增加,整个系统架构也变得稀烂,最后提出重构,《重构》一书开篇就指明了,重构是不能带来任何经济效益的事情。
每个开发或多或少都知道软件工程的几大要素,什么高内聚低耦合、开闭原则、里氏代换原则、依赖倒转原则等等,一谈起这些,口号说的震天响,一旦开发代码却全部抛之脑后,其实这些都是开发最本质的内容,一旦你遵守了这些原则,你的架构也不会乱到哪里去,我们今天所谈论的单元测试,一旦这些原则没做好,想写单元测试,做梦呢。比如A、B、C三个模块互相调用,你如何针对A做单测?
单元测试看着门槛低,实际上是一门很高深的学问,好的单元测试和系统架构设计能力分不开的。顾名思义,单元一词,指样本中自为一体或自成系统的独立成分,不可再分,否则就改变了事物的性质。那在系统中,何为最小单元呢?我们应当如何控制划分单元呢?如何把一个需求,拆分成一个个单元呢?好了,砖头抛出了,下面开始上干货。我将从进入公司第一个需求,多端权益同步这个需求来讲解,一步步讲解如何拆分单元,大单元拆成小单元,分别做单元测试。
理解单元
软件工程中对于单元是这么描述的:通常是指一个功能独立的部分,最常见的是单个函数或方法(模块)。对,在面向对象编程中,它可以是一个对象的公开接口或者方法。单元测试,就是针对于程序模块来进行正确性检验的测试工作。它和其他的测试策略一样,都是有三个核心的元素组成:
- 首先我们要明确知道,我们在验证什么。
- 其次我们要对执行的结果有一个验证。
- 准备测试数据
通俗的讲,就是你调用一个函数的input是什么,output是什么,作为开发你应该是知道的。不要扯什么一个方法中需要做很多业务逻辑,每个业务逻辑都是可以拆开,化整为零的。《clean code》这本书中指明,函数应该尽可能的短小,低于20行为佳,当然go语言的特性,我的建议是不能超过40行。
单元测试,有着以下的特征:
- 好的单元测试,是可以快速执行的,必须是毫秒级别的一个速度。
- 单元测试和单元测试之间,不允许存在相互之间的调用,当然咯,一些初始化的方法还是可以的。
- 单元测试必须是可重复执行的,也就是幂等性,如果一个单测在本地环境是执行通过,但是上集成环境或者其他环境执行不通过,那这样的单元测试也是有问题的。
- 单元测试,是支持自检查的,我见过一些开发,直接在main函数去调用自己的方法,或者是直接写一个接口来调用这个函数,也见过在代码中加一些日志的输出,导致无用日志大量增加。这些都是不对的,因为它没办法自检查,需要人工的参与才能知道测试是否执行正确。
为什么要写单元测试
在公司,我们做任何一件事情,都是需要这个事情价值的,而不是关注这个事情的本身,除非你是为爱发电。,artin fowler在《重构》中指出,测试一定是在你最但的地方去进行测试,而不是为了追求测试而去做测试
,当然墨菲定律也给出类似指引:人生往往是,怕什么来什么,越担心越会发生
,所以单元测试对我们来说只是一个工具,并非是一个目标,工具我们就要使用它,来帮助我们的代码写的更好,框架更稳定,得到的反馈更快,缺陷更少,从而获得信息。我们都有这样的经历:
对之前遗留的代码,不太敢去动他
如果有新需求,可能更愿意在一个独立的地方重新去写,而不是复用别人之前的东西,或者对别人之前的东西进行一个重构,造成这种现象的原因其实很简单,就是我们对原有的代码缺乏一些保护,而且是快速获得反馈的反馈,如果通过线上或者集成的测试来经过验证,这段代码是否有影响,但往往都是好几天之后,它不会给开发人员带来任何重构的决心。
我写出来的代码,不太放心交给别人去修改
因为我担心他会破坏原有方法执行的意图,或者破坏里面的我处理的问题的一些设计。
这种情况,就是我对自己写的代码和对别人写的代码,都不太放心,往往出现一种现象,就是在编程的时候是一种祈祷式的编程,会盼望别人的代码没有问题,或者祈祷别人不要修改自己的代码。
如果,有一个说明书(不是注释,我一直强调不要写注释,如果代码需要注释来解释这段代码是什么意思,说明你代码写的不好),可以快速的知道这段代码的输出是什么,相信你会大胆的修改,这个说明书,就是单元测试,它的核心价值就一个,就是缩短反馈的周期,在基于缩短反馈周期上,会发现单测带给我们更好的一些影响,是降低了修复缺陷的影响和成本,花费的人力、物力,从另外一个角度,就是为这个项目,公司节省了成本,提高了产出。
下图是微软针对测试的一些统计数据:
- 左图为缺陷在哪个阶段被发现的比例和花费的成本,我们可以发现在编码(coding)到单测(unit)的阶段被发现的比例为85%,这个阶段我们修复的成本是极低的,在功能测试(Function)和集成测试(field)我们发现的比例低但花费的成本是直线上升的,最后上了生产环境(Release),缺陷的花费成本是急速上升,消耗更多的时间、人力,和给团队带来不太好的影响。
- 右图为缺陷被发现反馈的时间,在单元测试阶段时间最短,集成相对比较长,最后在系统测试和发布上线后的域测试花费的时间是最长的,
以下是两个团队在进行单元测试和不进行单元测试下的各个阶段交付速度对比:
明显看出,虽然进行单元测试团队在codeing阶段,花费的时间为双倍,但是整体交付的时间却提前了三天,因为不进行单元测试的团队,不断的回归测试、修改bug等等的阶段,这只是一个按月的迭代,我们如果按年来看的话,这个提升的效率是客观的。关键是生产环境客户发现的bug数,不进行单测团队的数量,遥遥领先进行单测团队的数量,这说明很多bug没有经过单测,会被我们遗漏到生产环境上面去。
以上,可以说这就是单测给我们组织、团队、项目带来的价值和意义吧。
当然,还有一些附加的好处,就是对个人,对开发本身、代码质量是有更高的价值的,比如:
驱动设计
有本书叫做DDD领域驱动设计,里面有个领域模型,和我上面提到的模块化是一样的,它明确代码功能模块职责,帮助系统的灵活设计、松耦合,因为如果没有领域的分界,单测其实是运行不起来的。
活文档
任何的文档更新速度,远远没有代码来得快,这是最新的说明文档,就是存在代码中,运行单测,一目了然。
安全重构
在重构的过程中,可能会担心改变代码现有逻辑,如果有的单元测试,我们就会更有信心,因为我们可以持续的经过单测来验证有没有对代码进行破坏。
易于调试
在单测保证下,一旦发生线上事故,或者测试环境下的bug,可以更快的把需要的数据在单测中进行回放,去验证代码是否有问题,不断的去调整数据,直到在单测中可以重现问题,并且修复。因为线上环境的事故,都是按秒计算的,这都是绩效啊。
提升信心
这是最重要的,因为单测可以驱动设计、活文档、安全重构、易于调试等等价值,最终让我们对代码更有信心,提升对自己代码、别人代码的信心,这是我认为单元测试给我们带来最大的好处。
如何做好单元测试
根据阿里单元测试宣讲,单元测试编写流程分为四大步骤,八大方法。
第一步:定义对象阶段
1:定义测试对象
2:模拟依赖对象,参数或者返回值 3:注入依赖对象第二步:模拟方法阶段
2:模拟依赖对象,参数或者返回值 4:模拟依赖方法
第三步:调用方法阶段
2:模拟依赖对象,参数 5:调用测试方法 7:验证数据对象
第四步:验证方法阶段
6:验证依赖方法 7:验证数据对象 8:验证依赖对象
扩展
最近有一些同学,当然也包括工作多年的我,对于单元测试、集成测试、系统测试有一些疑问,最近我翻阅了一些资料,列举了一个表格,来说明这三者的区别。
单元测试 | 集成测试 | 系统测试 | |
---|---|---|---|
编写人员 | 开发 | 开发 | 测试 |
编写场地 | 代码仓库 | 代码仓库 | 代码仓库内/外 |
编写时间 | 代码发布前 | 代码发布前 | 代码发布前/后 |
编写成本 | 低 | 低 | 高 |
编写难度 | 低 | 中 | 高 |
反馈速度 | 极快,级 | 较慢,分钟级 | 慢,天级别 |
覆盖面积 | 代码行覆盖60-80,分支覆盖40-60% | 功能级别覆盖 | 核心保障链路 |
环境依赖 | 代码级别,不依赖环境 | 依赖日常或本地环境 | 依赖测试环境、预发环境、生产环境 |
外部依赖模拟 | 全部模拟 | 部分模拟 | 不模拟,完全使用真实环境 |