使用nose与mock对Python程序进行简单的单元测试
0X00 install
安装nose:pip install nose
安装mock:pip install mock
Python3 中mock模块已成为标准库,无需单安装
在任意目录下执行nosetests
看到有输出就是已经安装好了nose
。进入到Python shell
中执行import mock
没有报错也就是mock
安装好了。
0X01 用于测试的代码
这里先贴出这次被测的代码simple_math.py
,是一个非常简单的数字计算类。
1 | #!/usr/bin/env python |
这里的代码是有问题的,毕竟是要拿来作为单元测试的样例的嘛。
0X02 编写单元测试
我们要针对上述文件创建一个新的test.py
来测试其中的MyMath
类。
1 | #!/usr/bin/env python |
从代码中可以看到首先导入了需要测试的类MyMath
,然后就写了几个test_
开头的方法,方法内部是模拟调用MyMath
中的方法,并将得到的结果与预期结果相互匹配,最终使用assert
语法来判断是否返回了理想的值。测试代码写好之后在当前目录下执行nosetest -v
来运行我们的单元测试吧,输出结果如下。
1 | test.test_add ... ok |
0X003 简单解释一下
上面的测试就执行完了,那我们来拆分一下这个单元测试的流程吧。
- 首先我们执行了
nosetest -v
命令,这个命令就是开始进行单元测试的,其中-v
参数是用来展示更加丰富的输出的,如果不加这个参数的话运行结果会更清爽一些 - 执行命令之后nose会自己查找当前目录下名为
test.py
或其他以test_
开头的python文件,并且执行这些文件中编写好的以test_
开头的方法(就比如我们代码中的test_add/test_subtraction
) - 逐个执行上面找到的方法
- 输出最后结果
如果在执行单个test_xxx
方法的时候没有抛出异常,那么就认为这个测试(test case)是通过了的,如果抛出了异常则认为此处的测试不能通过。但是在单元测试中有一点与普通Python程序不同,普通Python程序遇到抛出的异常时就会层层上抛,而这里会收集展示出来,然后继续运行下面的测试。
0X04 换一个姿势写测试
还是上面的被测代码,这次不是单纯使用多个方法来完成测试了。面向对象的思想也可以对应的放到单元测试中,比如我们针对MyMath
类搞一个单元测试,这些测试内容也整理为一个类,不过值得注意的是**这里的类名一定要为TestClass
**否则是运行不到的。
1 | #!/usr/bin/env python |
0X05 setup与teardown方法
上面的测试中我们每一个test_case中都有一行mt = MyMath()
是不是感觉有些蠢,其实是可以避免这个方法的。定义两个方法setup/teardown
,这些方法在每执行一个testcase的时候都会执行,不同的是setup
执行在testcase之前,而teardown
执行在testcase之后。下面例子中就是这样的,每次使用mt
对象时都实例化一个新的,用完再删掉。
1 | #!/usr/bin/env python |
虽然上面的mt
对象没有必要每次生成新的,但是很多情况下其实是需要我们这么做的。考虑这么一种情况:有一个class Student
需要测试,而且待测方法有非常非常多,不仅会计算Student实例的各个属性,还要对其进行更新、添加、删除等操作。那么这种情况下每次操作都生成一个新的Student实例并且在testcase结束之后删掉它就是非常有必要的了。
因为我们不能保证每个方法的幂等性,比如万一这个Student是男的,你在测试了
student.change_sex()
方法之后显然他就不再是男的了。那接下来再测试一些依赖与性别的地方时就会出问题,比如get_gender()
方法你很有可能会写assert student.get_gender() == "Male"
,那这个时候就出错了。
0X06 模拟一些对象
对,你没有对象的时候可以模拟对象出来(hhh。
还是,首先有一个场景:工作中编写单元测试,被测功能简单说是“从某一接口拿到数据,并对数据进行处理”。那么我们就可以写成这么一个操作
1 | def test_operation_data_from_api(parms): |
这段代码看起来是没有问题的,但是如果get_data_from_api
调用的API是收费API呢,每次跑单元测试都要去请求一次吗?如果API巨慢无比,每次都要几秒钟才回得来,所有测试有需要调用上百次这个接口呢,我们就干等着吗?这显然是不合理的。此时就可以使用最初提到的mock
来模拟数据从而解决上述问题。
1 | import mock |
通过对response的模拟,我们可以实现不用真正去请求API就能继续测试的方法。因为我们这里编写测试的目的是正确处理response所以可以模拟response。如果我们的目的是测试get_data_from_api()
这个方法的话那就不能想这样模拟了。
0X07 Mock模拟其他东西
mock可以模拟一个属性,多层属性还能模拟方法。模拟多个属性与模拟一个属性是一样的,只需要一路.
下去就可以。中途遇到没有自己定义过的会自动生成一个mock.Mock()
放进去。比较有意思的是模拟方法,使用Mock.method_name.return_value
就可以模拟method_name
方法,并且return_value
就是这个方法的返回值。
1 | #!/usr/bin/env python |