单元测试浅析
1、何为单元测试
在计算机编程中,单元测试(英语:Unit Testing)又称为模块测试, 是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。
2、单元测试的优势
- 适应变更
单元测试允许程序员在未来重构代码,并且确保模块依然工作正确(复合测试)。这个过程就是为所有函数和方法编写单元测试,一旦变更导致错误发生,借助于单元测试可以快速定位并修复错误。 可读性强的单元测试可以使程序员方便地检查代码片断是否依然正常工作。良好设计的单元测试案例覆盖程序单元分支和循环条件的所有路径。
- 简化集成
单元测试消除程序单元的不可靠,采用自底向上的测试路径。通过先测试程序部件再测试部件组装,使集成测试变得更加简单。
- 文档记录
单元测试提供了系统的一种文档记录。借助于查看单元测试提供的功能和单元测试中如何使用程序单元,开发人员可以直观的理解程序单元的基础 API。 单元测试具体表现了程序单元成功的关键特点。这些特点可以指出正确使用和非正确使用程序单元,也能指出需要捕获的程序单元的负面表现(译注:异常和错误)。尽管很多软件开发环境不仅依赖于代码做为产品文档,在单元测试中和单元测试本身确实文档化了程序单元的上述关键特点。
- 表达设计
在测试驱动开发的软件实践中,单元测试可以取代正式的设计。每一个单元测试案例均可以视为一项类、方法和待观察行为等设计元素。
3、单元测试的局限
测试不可能发现所有的程序错误,单元测试也不例外。按定义,单元测试只测试程序单元自身的功能。因此,它不能发现集成错误、性能问题、或者其他系统级别的问题。单元测试结合其他软件测试活动更为有效。与其它形式的软件测试类似,单元测试只能表明测到的问题,不能表明不存在未测试到的错误。
4、单元测试的要求
4.1 代码可测性
这种情况可能是你代码本身导致的,首先你要写具有“可测性”的代码,这意味着你不能写面向过程的,流水式的,几百行逻辑堆一起的代码(也叫意大利面代码,就像一盘意大利面一样搅在一起的代码。),你要学一些模块化技巧,面向对象和函数式编程理念,还有很多其它具体方法,比如能用本地变量,就不要用全局变量等等,让你的代码具有可测性,这些知识的学习应该放在单元测试之前。
4.2 测试独立性
每个理想的测试案例独立于其它案例;为测试时隔离模块,经常使用 stubs、mock 或 fake 等测试马甲程序。不与外部(包括文件、数据库、网络、外部服务)直接相依。
4.3 测试有效性
测试代码和功能代码同时提交。
4.4 测试及时性
单元测试的目的是,针对修改的每个函数或方法,进行快速的测试,观察结果是否如预期。不说代码改动的效果如何(性能,规范等),至少可以正常运行。
4.5 一个测试案例只测一个方法
单元测试注重快速,小而美,针对一个实例方法,可能会有很多不同方向的测试,将这些测试方法分开。假设测试账户有效性,账户有效写个方法,账户无效写个方法。
5、单元测试之stub,mock,fake
这三个概念来源自面向对象设计语言在 TDD 方法下的实践。 stub 检测回调值return。我们用 pytest 或者 unittest 替代。 mock 模拟和外界的连接。我们用 python mock 替代。 fake 虚拟环境。我们可以显式修改临时变量。
6、python 常见单元测试框架
6.1 unittest
python 标准库自带的单元测试框架。
6.2 pytest
非常简洁好用的单元测试框架。简单使用方法可以参加博客pytest的简单学习
6.3 mock
单元测试应该只针对当前单元进行测试, 所有的内部或外部的依赖应该是稳定的, 已经在别处进行测试过的。
当功能代码中有需要调用外界资源时(例如连接 web,连接数据库,打开文件等),为了保证真实环境不被多次重复的测试搞乱,可以用 mock 模拟出一个对象,用完就丢,不影响外界资源。
举个例子,测试在数据库插入一个数据,第一次 ok,第二次就会报重复错误,还需要考虑当前数据库数据情况,同时打开数据库需要时间成本,算力成本等等。
7、项目中该怎么用
7.1 单元测试与重构
单元测试有个优势就是适应变更,这里可以理解为需求变化,也可以理解为重构过程。重构过程中可能会出现参数变化之类的变动(解耦合),之前写的测试代码会失去效果,此时应先用更大粒度的测试确定功能实现正常,可以使用selenium进行测试。当代码重构完成后,可以补充测试。
重构,应该针对需要的部分重构,且适可而止。
Ruddy Lee: 常常有工程師把只會被執行個幾回就一輩子不會再被執行到的程式;寫得完美的一蹋糊塗,真是太愛乾淨了。真是愛做白工… 還不如早一點睡來得有價值,起碼會活的健康些,程式只要在他被需要的生命週期內活得好好的,就ok了! 這就是done了。
7.2 针对项目基础方法
这里的基础方法指的是,只使用 python 语法,不使用任何包,返回一个结果的方法。使用 unittest 或者 pytest,多考虑边界条件就行了。
7.3 针对项目调用了标准库模块的方法
标准库的包相对稳定,理论上只要使用得当,不需要考虑 mock 问题。如果该方法返回结果, assert 这个结果和预想值就 ok 了。
7.4 针对调用三方库的方法
看情况,如果是 requests 库,就需要 mock 相关请求;如果是 bs4 库,让它调用处理,看 return 结果。
7.5 针对连接数据库或者网络资源的方法
单元测试的要义在于可重复快速的检测代码功能是否完整,所以测试时不要直接连接数据库;最好使用 orm 技术连接数据库查找数据,连接相关的代码抽出来单独测试。使用 mock 模拟出一些 object,测试当前方法的功能实现。即假设传进来一些正确的数据,则…,穿进来一些错误的数据,则…。
7.6 针对使用装饰器的方法
装饰器的作用是增加功能,实现装饰器的方法应该被单独拿出来测试。也就是说,使用装饰器的方法的测试就是,把它当做没有装饰器来测试。
7.7 针对阻塞方法
大部分的阻塞聚集在 IO,包括网络 io,文件 io,数据库 io 等,这就可以回到 6.5 条,使用 mock 方法模拟掉阻塞对象;但假设方法存在 cpu 阻塞,那么要么是代码写的不好(死循环),要么计算量极大,这时候的单元测试没什么意义。
7.8 针对异步方法
pass
7.9 针对无 return 的方法
没有确切方法测试,根据方法实现的过程制定特定的方法。 举个例子,启动监控这个方法,不返回值,过程中通过调用 monitor 类的方法启动监控,那么我们可以 mock 这个 monitor 类的方法,assert 下 mock 对象是否被调用就知道该方法是否执行。至于能不能真的启动监控,要在集成测试中才能看得出来。
8、附录
8.1 十九条建议
-
不应该只编写理想状态单元测试,应充分考虑各种情况。
-
一个测试案例只测一个方法,一种情形。
-
测试类具备可读性,确保测试类标有注释并且容易理解。
-
良好的命名规范,便于人们理解。
-
将断言从行为中分离出来,断言应该用来检验结果,而不是执行逻辑操作的。
-
使用具体的输入,对输入值有掌控。
-
把测试类分类,放在不同的地方。
-
好的测试都是一些独立的测试类,你应该让测试类与其他的测试、环境设置等没有任何依赖。
-
不要测试私有的方法,因为私有方法会被公有方法调用。
-
不要连接数据库或者数据源。
-
一个测试不要超过一个模拟(mock 对象,努力去消除错误和不一致性。
-
单元测试不是集成测试。
-
测试必须具有确定性,你需要一个确定的预测结果。所以,如果有时候测试通过了,但是不意味着完成测试了。
-
保持你的测试是幂等的,应该能够运行你的测试多次而不改变它的输出结果,并且测试也不应该改变任何的数据或者添加任何东西。
-
测试类一次仅测试一个类,测试方法一次仅测试一个方法。
-
在你的测试里使用异常,测试里会遇到异常,所以,请不要忽略它,要使用它。
-
不要使用你自己的测试类去测试第三方库的功能。
-
限制规则,当在一些规则下写测试时,记住你的限制和它们(最小和最大)设置成最大的一致性。
-
测试类不应该需要配置或者自定义安装,你的测试类应该能够给任何人使用并且使它运行。