从开源项目看 Python 单元测试
我觉得以前在我开发程序的时候,除了文档,可能单元测试是另外一个让我希望别人都写,但是自己又一点都不想写的东西。但是,随着开发程序的增多,以及自己对 Bug 的修改的增多,我发现,UT 在很大程度上是对我有利的,虽然带来的结果就是可能我的 Dev 时间会增加 20-40% 左右,但是,相比较于一段时间之后突然冒出来一个 Bug,让你摸不着头脑;或者说突然一个接一个的 Bug 在你转测试之后提过来,写 UT 的幸福感和自豪感明显是更高的。
就我目前而言,我认为写单元测试有这么几个好处:
- 帮助减小代码的耦合度,这样你才能更容易得编写 UT
- 理清代码的模块依赖,这样你才能在 UT 中知道哪些东西要 Mock,哪些东西要 Stub
- 保持接口的干净和明朗,UT 就是针对接口编程,Input 和 Ouput 都需要明确
- UT 是一个自诠释的文档,别人可以通过你的 UT 来学习你的接口使用方式
这里我需要提出一点的就是,一般而言,我很难做到 Test First,也就是所谓的先完成 UT 代码的编写,然后再写实现代码。当然,这不是说不行,就以目前的经历来说,这做法欠妥,一个很重要的原因是项目周期的把控,如果你 UT First,万一后面你时间不够实现 Logical Code 了怎么办?光有 UT 并不能让你的整个 Project 跑起来。所以,一般来说,我经历的大部分项目都是先 Run 起来,然后再通过 UT 保证目前的功能是正常的,并且可以保证在以后的维护和更新中,功能的正确性不会被破坏。
测试方法
学过完善的软件工程体系的同学都知道,软件测试是软件工程中非常重要的一环,甚至于可以说对于一个 Project,测试人员的参与度比 Developer 的高多了,我刚毕业那会,测试人员的参与度可以说是贯穿了全流程,从需求的提出到验收发布,这整个流程都有测试人员的参与,而 Developer,可能参与到"转测试"环境就差不多完了,所以测试工程也是一项非常复杂的学科。
测试覆盖
因为测试非常复杂,所以也是有很多方法论和实践的。就拿 UT 来说,对于代码我们可以有几个不同的测试角度。例如覆盖角度来说,我们就有语句覆盖,分支覆盖,条件覆盖,路径覆盖和循环覆盖;测试内容来说,我们又会分模块测试,数据结构测试,路径测试,错误处理测试和边界测试等等。对于这么多测试,其实我发现大部分开源项目都没有很严格得遵守这些理论,因为可能说随便一条理论在实践中都能让人抓狂。
其实在我见过的几个流行的开源项目中,基本上都是以语句覆盖为目标进行的,并且并不能达到 100%,所以更多得是以主要功能是正常的为目标进行 UT 测试的。以下是部分开源项目的测试结果:

除此之外,对于 UT 的增加是在 issue 的基础上建立的,也就说当有用户提了一个 issue 之后,Maintener 觉得这个 issue 是个问题,并且会影响到我这个项目,那么就会开发开发相应的 patch fix 它,并且补上 UT,这种情况也是比较常见,这样的话,渐渐地 UT 的覆盖率也就慢慢上去了。
测试方法
在测试中,我们的代码可能会有很多依赖,例如模块依赖,组件依赖等等,为了解决这些依赖,我们总要有一些方法来处理,这里就有两项经常使用的技术:Stub 和 Mock。
我以前喜欢说讲一个对象 Mock 掉,意思就是讲一个对象用自定义的模拟类替换掉,从而让我们可以自定义类的行为和输出,但是,我发现这其实在测试中是 Stub,所谓的 Stub 就是模拟测试代码调用的模块和组件,从而自定义被调用后的行为和输出;而相比之下,Mock 的功能是验证模板或者组件有没有被调用,很常见的例子就是邮件发送服务有没有被调用,有没有输出日志内容等等。关于 Stub 和 Mock 更多的内容介绍我推荐 Martin Folwer 的这篇文章:Mocks Aren't Stubs。
测试工具
在 Python 中,自身就带了类 XUnit 的 unittest 框架,使用也很简单,例如下面就是一个很简单的测试用例:

其实使用起来已经很简单了,但是 Python 的小伙伴还是嫌他又啰嗦又慢,所以你会发现 pytest 这个库很受欢迎。
pytest
pytest 作为一个单元测试框架,使用方法有多种,既可以和 python 自带的 unittest 类似,又可以很简单得就一个函数来写 UT;不仅开发效率会更高,而且执行效率也可以更高,其他优点就不啰嗦介绍了,官网里面都罗列了:pytest。
至于有多简单方便,你将下面这段代码保存到 test_sample.py
文件中,然后在对应的目录路径下执行 pytest
命令

执行之后你应该会发现:

对,你会发现,就这么简单得执行起来了。但是,很多同学还是不满于此,因为很多 Python 项目不仅仅适应于一个版本的 Python,所以就会有多一个 Python 版本的测试(Python 的又一坑,2.6/2.7/3.x/3.5 不兼容)。
tox
为了满足一个 Python 项目可以在多个 Python 版本中可以正常运行,很多人会使用 tox 进行不同环境下的兼容性测试,所以 tox 的功能就是环境管理和测试运行。关于 tox 的更多功能使用和细则可以参考一下 tox 官网:Tox。
tox 一般都会有一个 tox.ini 文件,例如一个简单的例子:

然后执行 tox
命令行工具就可以了,它就会找你机器上的各种环境,然后测试起来,最后的结果就有点类似于:

小结
单元测试是一种习惯,也是一种责任。通过单元测试,我们可以告诉别人我的代码是 Work 的,同时也给别人一种信任感,可以让别人相信你写的代码。同时,编写单元测试也是一项比较繁琐的事情,我们要处理依赖,考虑 Test Case,但是,这些过程都可以帮助我们更好得思考我们的项目和软件,从而让软件的结构和代码的质量提升一个台阶。