关于程序员:单元测试只是测试吗

6次阅读

共计 6512 个字符,预计需要花费 17 分钟才能阅读完成。

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


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

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

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

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

一 品尝篇

在品尝篇,一起看看什么样的代码才是易于单测的。

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

你可能立即就会产生和程序员小 A 相似的纳闷:” 无论代码写成什么样,通过 Mockito 和 PowerMock 必定都是能写出单测来?所以通过单测真的改善代码构造吗?”。

实际上,大量应用 Mock 工具的单测相当于买椟还珠,只具备测试的能力而无奈帮忙代码设计。

以一段非常简单的程序为例,假如这是一个商店零碎,外面有一个买面包的办法,外面会调用银行提供的信用卡服务 creditCardService 来扣除传入的信用卡的钱。这段程序如果应用 Mockito 的话,预计你很快就能写出测试了,只须要把 creditCardService 给 Mock 掉,而后验证它传入的参数就能够了。

如果总是像下面这样思考的话,单测对于你改善代码设计就没什么帮忙了。咱们在给代码写单测的时候不应该上来就思考用什么样的工具来测试代码,而是应该思考如何重构代码,能力让代码变得更加容易测试。

还是下面这段代码,咱们换个角度,思考下如何重构代码,能力让这段逻辑不须要 mock 就能测试?

其实非常简单的一个方法是,返回一个打算,而不是立刻就执行内部调用,比方这里咱们能够形象出一个 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。
  • 第四级,无奈单测:模块被设计的及其简单,连开发者本人都无奈了解,更无奈写单测。

二 实际篇

在上一篇学习了对于单测的正确观点后,这一篇再来聊一聊对于单测的最佳实际。

单元测试的运行速度重要吗?

很多人会感觉单测反正也不是零碎中的代码,运行的快慢无所谓,而后写出很多其慢无比的单测,以至于零碎全量跑一次单测要几十分钟。这样的话就齐全偏离了单测的定位,单测的目标就是为了不便疾速迭代,改了两行代码就能够在本地用 30 秒到几分钟的工夫全量跑一次单测来确定影响范畴,而不是每次都要通读零碎源码能力晓得改变的影响范畴,这样新人很快就能够大胆改代码了,而不是先花几个月通读零碎源码,或者先踩好几个坑,能力上手干活。那些全量跑单测要几十分钟的零碎,他的开发者基本就不会在本地全量运行单测,每次都在 aone 上跑半天才晓得单测不过,这样的单测就形同虚设了。

违反这个准则的典型反例,就是在单测中启动 Spring。

数据驱动测试(Data Driven Test)

不好的单元测试经常只用一组失常测试数据进行测试,实际上咱们应该应用多组数据,包含失常和异样数据,输出模块,看返回值是否合乎预期。应用多组测试数据是否就意味着多写很多代码呢?并不是,咱们只有留神将测试用例的逻辑与数据拆散就能够,测试代码顺次读取测试数据,校验其是否合乎预期。这样的逻辑与数据拆散的测试个别称做“数据驱动测试”,常见的单元测试框架都会提供这种反对。

“ 数据驱动测试 ” 的概念还是太形象了,这里咱们看两段代码,左图未分离数据与用例,右图则做了拆散,可能看出很显著的不同,右图是基于 Spock 单元测试框架来写的,不相熟的人看上去可能比拟奇怪,能够把 where 标签下的代码看成一张表格,每一行都是一组测试数据,Spock 框架会将其顺次代入 testAdd 办法参数进行测试。

大家所相熟的 junit 框架也是能够做的,然而须要写一个额定的外部类,加上 @RunWith(Parameterized.class),写一个 data 静态方法,而后返回须要测试的数据组,而后 junit 就会顺次将数据填入这个类的属性中,运行这个类中的全副测试用例。

如何测试公有办法

大家写单测时常有的一个困惑就是公有办法怎么测试?尽管实践上公有办法不须要写单测,然而有些公有办法逻辑比较复杂,还是值得独自写测试的,目前公认比拟好的实际就是将修饰符从 private 改成 protected, 这也是很多开源我的项目给单测留口子的办法。如果你的我的项目刚好有引入 guava 的话,能够再给办法加上一个 @VisibleForTesting 的注解,示意仅仅是出于单元测试须要批改的修饰符。

一个典型的例子:

三 TDD 与 BDD

最初一篇来讲一两个大家可能常常据说过的理念,TDD 和 BDD。集体感觉这两个理念都比拟极其,理论中很难利用,启发意义大于其实用意义,所以放在最初,心愿能带来一些启发。

TDD

TDD 强调让写代码的过程造成一个循环,第一步是为你要做的性能写一个单元测试,跑一下发现没有通过(毕竟你还没有实现代码),即图中的 TEST FAILS,俗称“红灯”,之后编写可能通过全副测试的“最小代码”,之所以强调“最小代码”,就是为了避免适度优化,事实中咱们常常会因为代码适度优化,或者适度设计,导致很多遗留问题,在这个阶段,只管用最快最脏的代码实现就好了,不必管太多设计问题。这个阶段俗称“绿灯”。

最重要的就是上面的“重构”(REFACTOR)阶段了,后面的代码尽管可能很脏,然而至多是正确,也有足够的测试来保障逻辑的正确,这个时候就能够大刀阔斧地重构代码了,保障代码持续放弃最优。

这启发咱们两点:

  • 单测必须可能疾速运行,因为单测是常常要在本地全量运行的,只有运行足够快,能力在 TDD 的循环中疾速迭代。
  • 好的代码并不是一次性就设计进去的,而是继续重构进去,而单测是继续重构的前提。

BDD

我经常埋怨产品经理在提需要时没有想分明,比方下图,如果让产品经理也能够写出可执行的测试用例的话,状况想必会好很多。BDD 就是这么一个想法。

不晓得大家有没有在有的我的项目里见过 .story 文件,它实质上就是一种集成测试脚本,只不过是用自然语言形容,它蕴含叙述,场景和步骤三局部,比方上图就是一个书店治理利用的 .story 文件,文件中叙述(Narrative)和 场景(Scenario)只是帮忙思考的,自身并蕴含在测试用例的逻辑中,测试用例次要由 Given, When 和 Then 结尾的语句组成,含意如下:

story 文件本人当然是无奈执行的,须要框架提供反对,JBehave 就是这么一种框架(右图),可能定义各种 Given,When,Then 语句的实现,下图的代码实质上就是个基于 Selenide 的自动化界面点击测试,它撑持 story 文件的执行。咱们以这个 story 文件为根据,就能够像 TDD 循环一样,先测试不通过(红灯),而后用最小的代码让测试通过(绿灯),最初重构代码。只不过这个循环可能会耗时好好几天,乃至几个星期。而 TDD 一个循环可能只须要几个小时,所以说 BDD 是集成测试版的 TDD。

麻利

咱们往往会感觉 TDD 和 BDD 会重大拖慢迭代速度,值得讥刺的是,TDD 和 BDD 恰好是麻利开发实际的重要组成部分:

咱们学习麻利开发的时候,经常只学习到它的“快”,而疏忽了麻利开发所提出的质量保证办法。麻利开发所谓的“快”,是指在代码品质充分保证下的“快”,而不是做完性能就间接上线。

四 如何学习写单测

学习单测的要害还是多实际,多看看他人好的单测怎么写。比方能够给一些公认代码优良的开源我的项目提交代码。

五 总结

  • 单测可能帮忙咱们验证代码设计的合理性。
  • 含有外围业务的代码应该首先思考如何让主体业务逻辑能够写无 Mock 单测。
  • 用例数据尽量和测试逻辑拆散。

参考资料

[1]Test-Driven Java Development
https://www.oreilly.com/library/view/test-driven-java-development/9781783987429/
[2]Wiki Agile software development
https://en.wikipedia.org/wiki/Agile_software_development
[3]PowerMock
https://powermock.github.io/
[4]JBehave
https://jbehave.org/
[5]Spock
http://spockframework.org/
[6]JUnit
https://junit.org/junit4/
[7]Learning to Love TDD
https://medium.com/swlh/learning-to-love-tdd-f8eb60739a69

正文完
 0