工作中,我习惯性的写单元测试。
我并没有受过专业的单元测试训练,也没有别人的指导和要求,是自己这几年工作下来,在没有外在要求的环境中,自己养成的习惯。
我知道这个习惯给我的工作带来了不小的帮助,这次把它写出来,是自己的一次总结,写给自己。
如果你读到了,也可以作为参考。
1. 什么是单元测试?
先看「维基百科」怎么说:
在计算机编程中,单元测试(英语:Unit Testing)又称为模块测试, 是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。
通常来说,程式设计师每修改一次程式就会进行最少一次单元测试,在编写程式的过程中前后很可能要进行多次单元测试,以证实程式达到软件规格书要求的工作目标,没有程序错误;虽然单元测试不是什么必须的,但也不坏,这牵涉到专案管理的政策决定。
每个理想的测试案例独立于其它案例;为测试时隔离模块,经常使用stubs、mock[1]或fake等测试马甲程序。单元测试通常由软件开发人员编写,用于确保他们所写的代码符合软件需求和遵循开发目标。它的实施方式可以是非常手动的(透过纸笔),或者是做成构建自动化的一部分。
下面是我个人理解。
1.1 是开发过程的一部分
「单元测试」虽名为「测试」,却是开发人员干的活,在开发阶段测试自己「即将写的代码」或「刚写好的代码」是否完成自己预期的功能。
在写代码的过程中,要知道自己在写的功能是干什么的,怎么知道这部分功能是对的,整个思考实现的过程,脑中一直都有个抽象的模型。当完成模型中的一小块功能后,马上验证下对与不对,再继续后面的工作。
1.2 是为开发服务的
我不太赞成强制为所有代码写单元测试,并过度强调测试覆盖,这样的结果会让我全身不自在,像被绑起来一样。写代码是要有一定的自由度的。
要求高的时候,单元测试是产品的一部分,要为产品的质量保驾护航。大部分情况下,单元测试是为我服务,而不是我去满足单元测试的各种要求。
换句话说,是因为它能给我带来帮助,对我有益,我才愿意去写它。
1.3 是一个验证的工作
我知道自己写的代码,大多数时候,不是一次就能写对的。
与其让别人告诉我代码有问题,不如我自己先验证一下。
1.4 是针对单一功能
单元测试是针对单一功能,如果功能很多,出现了问题将很难准确定位。
写代码应该是件简单的事,将多个简单的功能都写对了,再将之组合起来,出现问题的概率会大大降低。
单一功能更方便测试,功能多了,要测试的点也多了,单元测试往往没办法写下去。
2. 为什么要写单元测试?
如果没有外在的要求,可能很多人不愿意写单元测试。我为什么要写单元测试?
2.1 直接写出来的代码,往往是不对的
或许有人能将代码一次写对,这是我羡慕的。
我也想一次把代码写对,但我的经验告诉我,这对我太难,难到几乎做不到。
于是我对自己降低了要求,不要求自己第一次写出来的代码是对的,要求自己每次提交的代码是对的。
在提交代码之前,多测试一下,这样做到的概率大大提高了。
2.2 更低的调试成本
自己要测试,用的Java又是静态语言,Hotswap并不是很有效,经常需要重启JVM。
如果做的项目小,启动时间在10秒以内,那也罢了。
大多数时候,不是启Tomcat,就是启Weblogic,启动时间都是分钟级别的,这样测试验证的时间成本太高了,改代码10秒钟,测试一下3分钟,结果是:更少的测试。
用单元测试就好多了,就算用Spring加载一大堆东西,启动时间也在10秒内。
2.3 代码修改后,仍需要验证
有句话叫:代码不是写出来的,是改出来的。
很少有代码是写出来不用修改的。或是因为需求变了,或是因为前期没有考虑周全,或是因为实际情况与预期的不完全一致。
不管什么原因导致的修改,改了总需要再验证一下,看看改的是不是对的。
所以,单元测试不只是第一次写的时候有用。如果换个人过来改,有没有 单元测试,区别不是一般的大。
2.4 验证部分功能
如果不写单元测试,想测试部分功能,往往是做不到的。组合在一起是一个功能,内部有很多小功能组成,而出问题是其中的一个,要测试这一个的需要满足N个条件,这种情况下,测试变成了个麻烦事。
有了单元测试,就方便多了。写代码的人,更愿意用代码解决问题,而不是花很多时间去搭建测试环境、创造测试条件。
3. 如何知道该写一个单元测试了?
代码分2种:一种要单元测试的,另一种是不需要写单元测试的。
如何区分这2种呢?
3.1 一个独立的功能
业务上是独立的功能,一般是要写一个测试Case的。
功能的划分,是设计阶段 要完成的。
独立的功能是单元测试的必要条件。
如果一个人说,他的代码没办法写单元测试,我会将之等价于:没有设计,没有模块化,自己都不知道在写什么,代码一团糟。。。
这种情况,不改代码真的没办法写单元测试。
3.2 容易出错
一个地方是否容易出错,写代码的人是能感觉到的。
比如:我对Mybatis写的查询,全部要做单元测试。因为它的SQL是写到XML文件中,没有编译器的保证,肉眼识别没那么靠谱,测一下就放心多了。
3.3 是接口
接口几乎总是满足测试条件的,也几乎总是要测试的。
3.4 有算法
用Java写的代码,需要设计算法的时候并不多,一旦有,就需要认真对待,算法要考虑周全嘛!
所以,写算法的代码时,就要准备好各个测试点。
4. 怎么写一个单元测试?
写单元测试要做哪些事?
4.1 测试的功能本身独立
独立的功能是一个基本要求,功能或大或小,要独立起来算一个功能,有输入输出,更直白的说,有一个函数,最好是纯函数。
如果没有,先安全的处理下代码。
参考书籍:《重构,改善既有的代码设计》。
4.2 工具生成测试代码框架
这是开发工具都会带的功能,根据源代码,生成测试这些代码的框架性的代码。
比如:Intellij IDEA的「JUnit」插件。
4.3 基本功能的测试
常规的输入输出,必做的。
4.4 异常Case测试
我是会分情况来做异常Case测试。经常将之省去。
4.5 性能测试
用单元测试跑跑,测一下性能也无不可。代码灵活嘛!
用单元测试的功能,启若干个线程,压一压性能,有时还是比专业工具更得心应手。
5. 方法与技巧
如何知道自己测试的结果是对的?
5.1 Print
将输出打印出来,自己判断一下。
没办法自动化,要靠人来判断。
用起来最简单。
还可以再偷个懒,debug启动,加个断点,连输出都不需要了。
5.2 Assert
单元测试自动化的基石。
5.3 Mock
复杂的单元测试,能避免最好,如果一定需要,那就用吧!
比如下面的情况:
当类A中有属性B 和 C,B中又有D和E时,为了测试A,需要先创建4个对象b,c,d,e,而我们却不测试它,这不合道理。
我们可以做2个假的对象mockB和 mockC,用假的对象返回期望值,来测试类A。
6. 例子
最后,放几个我曾经写的单元测试,作为例子。
6.1 简单的Print
1 |
|
6.2 一个Assert的例子
1 |
|
6.3 复杂一点的
使用Mock,自定义返回值。
1 |
|
6.4 更复杂一点的
下面是一个更复杂的例子,在使用Mock的基础上,增加了内部调用方法次数的验证。
1 |
|
6.5 测试性能
自己写几行代码,简单的测测性能。
这是不是单元测试,似乎并不重要。
1 |
|