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


至此,你应该对如何写单测有点感觉了,我简单总结下上面说的几个小点:

  1. 单测不应该启动整个项目(包括 Spring 容器),没有这个必要,耗时长
  2. 单测不应该关心依赖的服务,包括 Dao、provider等其它服务,需要通过 mock 来解耦
  3. 一个测试方法只测当前要测试的一个类中的一个方法
其实就是分而治之的思想,本身在写代码的时候你已经为了降低复杂度和解耦,把代码分成了一个一个模块,一个个方法,而单元测试的目的,本就是验证这些你拆分的方法自身逻辑的正确性 。
为什么单测这么难写在对单测有点感觉之后,我们再来盘一盘为什么单测这么难写 。
核心原因在于, 我们本身写的代码不够解耦  。
看到这有人不服了,什么?单测难写还怪我本身写的代码不好,难写是因为本身的业务逻辑复杂!
好吧,这里需要强调一下,逻辑简单的类,其实没必要写单测,一般只是领导要求纯粹的追求覆盖率的时候,才会把这种简单的类补上去 。
举个很简单的例子:
studentService.getStudentById(Long id) ,我相信你都能脑补里面的逻辑,你要说你就想为这样的方法写单测,这当然可以,但是收益不大 。
单测收益最高的就是针对那些复杂的场景,比方说在开发周期比较紧急的时候,核心的、容易出错的逻辑才是更应该去重视的地方(要是开发周期空闲,你要补哪都行)
回到单测难写的问题上,用专业术语来讲,就是 你写的代码可测试性不高 ,导致难以编写对应的单测类 。
怎样的代码是可测试性不高呢?我举个非常简单的例子:
该死的单元测试,写起来到底有多痛?

文章插图
 
假设你要给 garbageMethod 写个单测,是不是有点难?
里面用到了静态方法,又 new 了个service 。
这静态方法我想让返回值等于 111,我只能去研究里面的逻辑 。有人可能想不就是一个方法的逻辑吗,就看看呗 。
那就看看:
该死的单元测试,写起来到底有多痛?

文章插图
 
可能你会说,这两分钟我就看明白了,但是这才一个,要是好多都需要看呢?
你为了测试当前的方法,且花了一堆时间去理解别的不需要测试的类的逻辑,这做法本身就不符合逻辑 。
然后那个 noSevice 是 new 的,这如何控制它的返回值啊?我想 mock 这个类也替换不了啊!
所以,这样的代码就是可测试性低的代码,不好 mock (当然,mock 框架支持静态方法的 mock,不过new noSevice 不好弄,当然一般人都有不会这样写的,我只是为了举例)
还有各种类之间有继承关系的,这种测试难度都比较大 。
就是上面的种种原因,导致我们的单测难以编写 。
所以如果我们在设计接口的时候,先编写单测,我们写出来的代码其实可测试性就很高了,因为你完全晓得这样的写法会使得你单测很难进行下去,自然而然你写的代码就会往解耦的方向发展(比如上面的 noService 肯定会注入) 。
我来列举下具体哪几种代码写法使得我们单测难以编写:
  1. 静态方法(不好mock替换注入,不过现在mock框架已支持)
  2. 内部直接 new,强依赖,无法 mock 替换注入
  3. 继承类,测试当前类的方法逻辑,还需要关心父类逻辑和mock父类的服务(所以我们常说组合优于继承)
  4. 全局变量,这个应该好理解,好方法都公用,你改了值之后,会影响别的测试类,特别是并发执行测试类时,就傻了
  5. 时间等一些未决行为,代码里面有 new Date,逻辑是近 15 天可行,然后超过 15 天就跑不通了(当然可以通过动态计算时间)
这里我要强调下,我不是说上面的这几种代码不能写,这是不现实的,我只是列举说明这几种可能会使得你的单测不好写, 当然第 2 点就是不能写的  。
写个单测例子说了那么多,不如实战一下,我就拿 trainingYes 来举例说明,这里引入 mockito 测试框架 。
可以看到,通过注解 mock 了需要 mock 的 dao 和 provider,然后将其注入到我们要测试的 yesService 中 。
该死的单元测试,写起来到底有多痛?

文章插图
 
接下来就是具体的逻辑,根据场景我一共写了 4 个方法来测试:


推荐阅读