持续交付2.0

单元测试实践总结

Image

本⽂对单元测试不做理论分析,只从功利的⾓度出发探讨⼀下⼯程实⽤的规则和⽅法,所以不会回答Utest和TDD有什么区别之类的“学术”问题:)

⾸先不可避免要回答的⼀个问题是:“为何要做单元测试?”个⼈的答案是“这是保证你写的代码是你想要的结果的最有效办法!”当然如果你有更好的办法,请不吝赐教。

没有完备单元测试的代码构成的⼀个系统,就像组装⼀架飞机,各个配件没有分别经过严格检 验,只在最后组装好后通过试飞来检验飞机是否正常⼀样。尽管软件开发可以“开着飞机换引擎”,但万⼀引发了线上事故,影响了绩效,减少了发量,这样的成本还是太⾼了。所以优秀的coder总会想尽⼀切办法保证⾃⼰的出品没有质量问题,单元测试就是⼀个有⼒的武器,可以⼤幅降低⼤家上线时的紧张指数。

下⾯试着回答这⼏个常见单元测试问题。

1、什么是好的单元测试?

2、单元测试测试什么?

3、什么时候写单元测试?

4、什么时候可以不写单元测试?

5、谁来写单元测试?

6、怎么写单元测试?

7、怎么写单元测试?(More)

1

什么是好的单元测试?

单元测试需要正确,清晰,完整,健壮!

  • 正确是不⾔⽽喻的。

  • 实践中单元测试不光测试代码的正确性,还能够帮助其他开发者理解代码逻辑,理解如何使⽤相关的类或者函数(可以当做接⼝或函数的使⽤⽰例了,省得写使⽤⽂档和demo了),所以要求单测写得清晰、简洁、有⾮常好的可读性。

  • 另外完整性也是必需的, 单测应该有很⾼的覆盖率,把可能的输⼊输出场景都考虑到。

  • 健壮是⼀个容易被忽略的要求, 当被测试的类或者函数被修改内部实现或者添加功能时,⼀个好的单测应该完全不需要被修改或者只有极少的修改。⽐如⼀个排序函数的单测实现是完全稳定的,它不应该跟着不同的排序算法⽽变化。

Image

2

单元测试测试什么?

我理解单元测试在绝⼤多数场景下其实是⽩盒的测试思维,即测试的是”What“,⽽不是”How“。

还是上⾯的例⼦,排序函数的单测只是验证任意⼀个序列是否能被排序函数输出正确的排序结果,⾄于排序算法实施细节的正确性是不需要被关注的。

另外注意把”What“拆分成⼀系列不同的⾏为,⾏为就是对不同的输⼊场景有不同的输出,每个⾏为都需要独⽴的单测。这样有新的⾏为引⼊时,就不需要修改已有的单测了。⽐如上⾯的排序函数,⾄少需要两个单测,⼀个是⽆重复输⼊,⼀个是有重复输⼊;⽽不是只⽤⼀个有重复输⼊的单测。

顺便说⼀句,Behavior-driven Test, Test-driven Development!

3

什么时候写单元测试?

好的习惯是在类或函数实现完成的时候就写单元测试了,但本⼈更多的时候是同时在写代码和单元测试。因为这样既可以在实现的过程中随时执⾏单元测试调试,同时也避免分开写时要重复理解⼀遍设计需求,⼤⼤提⾼了效率,节约了时间。

这⾥特别要强调的⼀点是,有时候写不写单元测试会直接影响到code的设计和实现。⽐如要写⼀个有很多条件分⽀处理的函数,如果不考虑单测,你很可能把这所有的逻辑写在⼀个函数⾥。但是如果考虑到单测实现的简洁,你就会把各个分⽀各写成⼀个函数,然后把分⽀逻辑另写成⼀个函数,最终意外达到了优化代码的⽬的。所以评判代码或者设计好不好的⼀个准则是看它容不容易测试。

Image

4

什么时候可以不写单元测试?

在个⼈的⼯作实践中,很少遇到可以不写单元测试的情况,当然确实有过不⽤写的时候。

1.函数特别简单,逻辑很直接,甚⾄于可以直接inline的函数。

2.函数逻辑太复杂了,历史上也从没有⼈为它写过单测,代码的Reviewer也没有要求我写。

3.代码的重要性不够,都是个⼈写个⼈维护的,即使代码有问题也不会有什么重要影响的。

4.有些接⼝的⼤函数,典型如Main函数.....

5.写对应的单元测试⾮常复杂,甚⾄⽆法写。这时候很可能1)需要修改设计,必须让你的设计易于单元测试;2)需要增强单元测试框架,框架功能不够,不能很好⽀持某种场景下的单测。

6.实在想不起来还有什么情况.....Maybe有些涉及到⽤户交互的UI单元?

5

谁来写单元测试?

相应类或函数的开发者来写单元测试,这是最⾼效的。不要让测试同学来写单元测试,他们有更重要的事情需要做,⽐如单元测试框架、Mock框架、测试基础设施、⾃动化测试、集成测试,及各种测试⼯具等等。

6

怎么写单元测试?

单元测试的代码结构⼀般是个三部分经典套路:准备,调⽤,断⾔。

  • 准备部分的⽬的是准备好调⽤所需要的外部环境,如数据、Stub、Mock、临时变量、调⽤请求、环境背景变量等等。

  • 调⽤部分则是实际调⽤需要测试⽅法、函数或者流程。

  • 断⾔部分判断调⽤部分的返回结果是否符合预期。

每个单元测试都应该能清晰地分出这三部分,当然有时调⽤断⾔两部分合在⼀起也是⽐较常见的。

7

怎么写单元测试?(More)

每个单元测试应该有个好名字,让⼈⼀看就知道是做什么测试,如果名字不能说明问题,就要加上完整的注释。⽐如 testSortNumbers_withDuplicated, 意味SortNumbers函数的单元测试应该可以处理「出现重复」的场景。

好的单元测试是完备且不重复的测试场景,或同类型的测试输入不要写多个单元测试。找一个有代表性的场景输入就可以了。

避免使用命令式编程(imperative programing),避免引入判断和循环等复杂逻辑,否则很可能会给单元测试自身带来不少bug,这样就需要给单元测试写单元测试了。

单元测试针对的是所有单元的对外接⼝,对外⾏为,⽽不是关注于⼀些内部的实现或者内部逻辑。另外要保证单元测试的外部环境尽量和实际使⽤时是⼀致的,不要给单元测试开任何的后门(Mock除外),也不要去测试⼀个被修改了的单元,如为了测试⽅便,继承了 ⼀个被测试类,然后修改它的某些⾏为⽅便测试。

……(以后想起来再补充)