lsekfe 发表于 2020-9-29 13:11:04

单元测试,只是测试吗?

推广单元测试,仅仅达到单测覆盖率是远远不够的,我们还要学习写"易于测试"的代码,以及"好"的测试,这样才能让单测真正发挥作用。本文将分享作者关于单元测试的思考与实践。

  首先我就来回答一下标题提出的问题:单元测试除了是一种测试手段外,更是一种改善代码设计的工具,容易写单测的代码往往也具有更加良好的设计。

  因而是任何自动化测试工具都无法取代的。

  当然,这里也不是把自动化测试工具给一棍子打死,自动化测试工具也有自己的使用场景,比如测试遗留代码,做长链路测试等等。

  这里需要强调一下 "工具" 属性,工具能放大人的智力或者体力,让干活的时候不会这么累,比如你去种树带把铲子,你肯定不会把铲子当成负担的,因为他是你种树的工具,你写 Java,肯定不会因为 IDEA 启动时间长,就把它当成一种负担,因为 IDEA 也是你写 Java 的一个工具,很多人把写单测当成一种负担,往往就是没有意识到"单测"是一种工具,单纯把他当成一种测试。

  Mock工具的使用——毒药还是解药

你可能立刻就会产生和程序员小 A 类似的疑惑:"无论代码写成什么样,通过 Mockito 和 PowerMock 肯定都是能写出单测来?所以通过单测真的改善代码结构吗?"。
  实际上,大量使用 Mock 工具的单测相当于买椟还珠,只具备测试的能力而无法帮助代码设计。

商店系统案例
  以一段非常简单的程序为例,假设这是一个商店系统,里面有一个买面包的方法,里面会调用银行提供的信用卡服务 creditCardService 来扣除传入的信用卡的钱。这段程序如果使用 Mockito 的话,估计你很快就能写出测试了,只需要把 creditCardService 给 Mock 掉,然后验证它传入的参数就可以了。
  如果总是像上面这样思考的话,单测对于你改善代码设计就没什么帮助了。我们在给代码写单测的时候不应该上来就思考用什么样的工具来测试代码,而是应该思考如何重构代码,才能让代码变得更加容易测试。
  还是上面这段代码,我们换个角度,思考下如何重构代码,才能让这段逻辑不需要 mock 就能测试?


返回一个执行计划,而不是立即执行外部调用:

上层拿到一个 Payment 实体后,可以选择立即执行,或者稍后统一执行。
  其实非常简单的一个办法是,返回一个计划,而不是立即就执行外部调用,比如这里我们可以抽象出一个 Payment 实体,表示从银行卡里划了多少钱,外部拿到 Payment 实体后再决定是立即把钱划掉,还是稍后把钱统一划掉。此时这一段逻辑不需要 Mock 就可以测试了,只要校验方法返回的 Payment 对象里面的属性是否正确即可。
  到这里,你可能又有疑问了,“费了这么大事重构代码仅仅是为了好写单测,值得吗?”,如果你有这个疑惑的话,那你可能还是把单测仅仅当成测试了,我之所以要把代码重构的好写单测,是因为好写单测的代码还有其他诸多好处。
  易于单测的代码仅仅是易于单测吗?
  更多的性能优化机会。
  就上面重构的代码为例吧,因为业务层返回的都是 Payment 对象,我可以这些 Payment 聚合起来,最后统一执行,比如下图的这段代码,我就可以把 Payment 按照银行卡分组统一扣钱,这样就可以减少 rpc 调用的次数,以后如果有需要的话,甚至可以直接将 Payment 作为消息发出去,到另一个系统执行,业务层根本无需关心 Payment 最后是怎么执行,只需要在付款的时候生成一个 Payment 就可以了。



更加健壮的系统
  另一个更大的好处是,好写单测的系统往往比不好写单测的系统更加健壮,如果一个系统大部分代码都可以写无 Mock 单测,那么它看起来就像左图一样,外部调用只是薄薄的一层,可以随意更换。
  如果你的系统大部分代码都一定要 Mock 才能测试的话,或者根本无法测试的话,就像右图一样,说明你的业务根本就没有自己的核心逻辑,而是和各种外部调用缠绕在一起。
  另外需要说明的是,图中红色的部分才是单测真正能够起作用的场景,因为它是比较稳定的业务逻辑,而且红色部分的单测也比较好些,只需要传几个参数进去,然后校验一下返回值就行了。灰色的外部调用部分理论上不写单测也无所谓,因为外部调用是不稳定的,即使你跟对方约定好了出入参数,他依旧有可能返回不符合约定的参数,或者直接就发生了网络错误,这一部分是集成测试发挥的场景。为什么在我们的系统里,大家都觉得单测没用,其实我也觉得单测对我们现在的系统没什么用,因为我们现在系统的主体代码就像右图一样,大部分都是灰色的外部调用,单测能够发挥作用的领域少之又少,即使写了覆盖率 80% 的测试用例,又能测出来啥?
  这里要再补充一下,我上面所说的 “稳定” 的含义,我说红色部分的“业务核心代码”稳定并不是说业务一成不变,业务肯定是一直在变的,而是说它的逻辑不会收到外部系统错误的影响,不像灰色部分,外部系统一抖动可能就会出问题,因为灰色部分不适合单测。
  Mock 工具的定位
  ·刚刚喷了这么久 Mock 工具,那 Mock 工具真正的定位究竟是什么呢?
  ·Mockito 是用来测试少量的不得不进行外部调用的代码。
  PowerMock 是用来测试设计得不好的遗留代码的。
  在 PowerMock 的文档中已经给出了警告,滥用它带来的坏处或许比好处更多,所以当我们写单测的时候不应该上来就想着用这些 Mock 工具,而是应该想想如何重构代码才能避开这些工具的使用。
  PowerMock 官方文档的警告:
  Putting it(PowerMock) in the hands of junior developers may cause more harm than good.
  另外,我们再聊聊单测自动化生成工具,我们刚好也有澄沨在做,无论是哪种单测生成工具,你会发现工具生成的单测到处都是 Mockito 和 PowerMock,显然不符合单测的定位,但是这种工具也是有意义的,当系统里到处都是不好写单测的遗留代码时,用这个工具生成一下也能帮助我们覆盖一小部分测试,对于我们系统目前的情况还是很有必要的。
  再来一个重构的例子

 写有外部调用的静态方法:

 最后的结果:

为了加深大家印象,这里再举个一个例子。比如下面这个方法,我在静态方法中调用先通过对 Business 的对象的各种处理,拿到了 rpc 调用的地址和版本号,然后使用这个地址和版本号加载一个初始化好的 hsf(阿里内部使用的 rpc 框架)泛化调用对象返回,这个方法的单测显然十分难写,因为 init 会发生网络调用,导致测试失败。这个时候我们要反思一下单测不好写的原因,是因为我们违背了一条编码的基本原则——“不能在静态方法中写外部调用”,如果你就是想在静态方法中进行外部调用,那应该怎么办呢?还是像之前的例子一样,返回一个计划,让外部调用,首先保持代码无副作用的部分不动,这一部分本来就没有外部调用,放在静态方法里执行也什么事情,然后把外部调用部分封到一个 Operator 里面(比如这里就是 RpcLoader)返回给上一层,上一层自己选择立即调用还是稍后调用。
  这么做除了好写单测,还有什么好处呢?最显而易见的一点就是代码变得可复用了,更重要的一点是防腐,你会发现 hsf 影响范围被局限在 RpcLoader 里面,以前哪怕它的 API 出现什么变化,或者要换别的框架,都是件非常容易的事情。
  为什么单测能够验证代码结构的合理性


前面我提到的这些关于代码结构的概念听起来是不是非常耳熟,在别的领域也经常听到,比如面向对象中的“高内聚,低耦合”,DDD 中所提到的“核心域”,“防腐层”,函数式编程所倡导的“隔离副作用”,你会发现,好的编程范式倡导的东西都是类似的。
  上面这三种评价代码的方式其实都是比较“主观”的,什么样的代码才能叫“高内聚”,在每个人看来可能都不一样。但是对于是否易于写单测,大家的标准基本是一样的,难写单测的系统给谁都很难写。而好写单测的代码一般都满足编程范式所倡导的原则,所以写单测的难易程度可以作为一个非常客观的代码质量评价指标。
  如果有人跟你说他这段代码设计得非常好,但是就是不好写单元测试,千万不要相信他。
  另外再提一下设计模式,如果只是照着书抄抄代码,设计模式是非常简单的,关键是要用对场景,一不小心就会只学到了“形”,而没有学到“神”,“形神兼备”的设计模式往往会让代码变得更加容易测试,如果用了设计模式发现系统变得更难测试了,那设计模式十有八九用得不对。


如果有个程序员跟你说我程序的性能达到了多少 QPS,你肯定会立马拿起测试工具就去测,看到底能不能到达这个 QPS。但是如果有程序员画了框框图说他的代码分成了 A B C 模块,要怎么验证他的代码真的分成了这几个模块呢?很简单,你看看每一个模块能否脱离其他模块单独测试就可以了,如果单独测试非常困难,那就说明模块并没有真的分开,而是或多或少耦合在了一起。
  易于单测的等级
  现在我们可以总结易于单测的几等级了。和别的领域不太一样,别的领域你高级的工具用得越多,可能越厉害,但是在单测这个领域,使用越多的高级工具,反而是更加糟糕的测试。
  另外,对这些规则也不要死脑筋,这些只适合业务含义比较丰富的代码,如果你就是在写一些封装外部调用的代码,这部分代码我觉得不写单测也是可行的。
  ·第一级,易于单测:大部分代码不需要 Mock 就可以测试,少量的外部调用代码需要 Mockito。
  ·第二级,能够单测:超过一半的代码需要 Mock 才能测试,但是这些测试也不是特别难写。
  ·第三级,难以单测:大量 Mock,甚至大量使用了 PowerMock。
  ·第四级,无法单测:模块被设计的及其复杂,连开发者自己都无法理解,更无法写单测。


页: [1]
查看完整版本: 单元测试,只是测试吗?