该死的单元测试,写起来到底有多痛?

到底什么是单元测试这个问题看似非常简单,单元测试嘛,不就是咱们开发自己写些测试类,来测试自己写的代码逻辑对不对 。
这句话没有问题,但是不够准确 。
首先我们要明白,这个测试二字前面还有两个字: 单元  。
它要求我们的测试粒度,小具体来说就是一个 Test 仅测试一个方法,对这句话的认识非常重要 。
市面上常见的错误单测是怎样的呢:
把整个项目启动,开始玩真的调用,入参是数据库里面真的值,所有的操作都落库,一个 Test 从 controller 到 service 再到 dao,一条龙打通 。
这种不叫单元测试, 这叫集成测试  。
如果你现在写的是这样的“单测”,你就会发现,写个测试类不仅要依赖数据库,还要依赖缓存,依赖公司别的团队的服务,亦或是一些三方开放平台的 Http 服务 。
当我们的测试类需要依赖太多太多外部因素的时候,只要有一个地方出现问题,你的测试就是 fail 的 。
并且入参和出参不能“任你摆布”,你还得想着如何控制别的团队的服务返回你想要的数据 。
比如我想测试当依赖的服务 A 返回 sucess 时,我的代码逻辑的正确性,还得想测试服务 A 返回 fail 的逻辑,还想测试它返回 null 的逻辑 。
再包括数据库或者缓存的一些返回值的定制,这非常的困难,已经开始劝退人了 。
然后 把整个项目启动 ,这通常需要花费数分钟甚至数十分钟的时间,写两个单测一下午过去了,时间都花在调试的启动上了 。
所以才会有那么多程序员觉得,单测好难写啊,又耗时,还动不动就 fail,写个 P 。
所以回过头来看,到底什么是单测?
在 JAVA 中,单元测试的对象是类中的某个方法,一个 Test 只需要关心这个方法的逻辑正确性,仅仅测试这个方法的逻辑,不应该也不需要关注外部的逻辑 。
举个例子,当你写 service 的单测时候,你压根就不应该测试 dao 或者外部服务返回的对不对,这是属于它们的逻辑,跟我 service 没有关系 。
可能听着感觉不强烈,我拿代码举个例:

该死的单元测试,写起来到底有多痛?

文章插图
 
假设我们要测试 trainingYes 这个方法,可以看到方法内部依赖 yesDao 和 OneOneZeroProvicer ,一个是数据库,一个是 RPC 服务 。
这时候我们的思维应该是:不管传入的 id 在数据库中对应的 yes 数据到底如何,我想让 yesDao 返回 null 的时候它就要返回 null,想让它不为 null 就不为 null 。
对 OneOneZeroProvicer 也是一样,我想随意操控让它返回 false 或者 true 。
因为数据库和外部服务的逻辑跟我当前的这个 service 方法没关系,我只需要拿到我应该拿到的值来测试我的方法内部的所有逻辑分支即可。
只有这样,我们才能容易的测试到我们所写的代码逻辑 。
【该死的单元测试,写起来到底有多痛?】你想想看,如果你要是测着 trainingYes 还得管着到底哪个 id 能拿到值啊,然后这个 yesDao#getYesById 内部逻辑有没有状态过滤啊,这个 id 对应的数据有被废弃吗,需要关心这个那个,这就非常累了 。
再或者你想关心 OneOneZeroProvicer#call 怎样才能返回 true,怎样才能返回 false,这就更难了,因为这是别的团队的服务,你连这个服务的代码权限都没,一个一个去问别人?
万一没这样的数据呢,还得去造?
总而言之,单元测试仅需要关注自己方法内部的逻辑,不需要关注依赖方 。
看到这,很多同学就搞不懂了,那该怎么搞?我的代码就是依赖它们的服务了啊 。
这就涉及到 mock 了 。mock 指的是伪造一个假的依赖服务,替换真正的服务,在上面的例子中,需要伪造 yesDao 和 OneOneZeroProvicer ,我们操控它得到我们想要的返回值,满足我们自身对 trainingYes 的测试需求 。
我拿 yesDao 举例一下,如下所示,我 mock 了一个假的 dao:
该死的单元测试,写起来到底有多痛?

文章插图
 
然后 在单测时通过反射或者 set 注入的方式把 MockYesDao 注入到测试的 YesService 中,这样一来,是不是就能控制逻辑了?
当我传入的 id 是 1 的时候,百分百拿到一个不是 null 的 yes 对象,当传入其他值的时候,肯定拿到的是 null,这样就非常容易控制我要测试的逻辑 。
当然,上面仅仅只是举例说明 mock 的含义的具体作用方式,实际上真正单测的时候没有人会手动写 mock 服务,基本上用的都是 mock 框架 。比如我用的就是 mockito,这个我们后面再提 。


推荐阅读