课件
Python 单元测试之 unittest
Python 测试框架 unittest 是受到 unit 启发产生的,并且内置在 Python 中,无需安装。unittest 框架主要由以下几部分组成:
- TestCase 类:它是最小的测试单元,通过集成这个类来编写测试用例,一个测试用例包含若干个以 Test 开头的相互独立的测试方法,也可以使用 SetUp 和 tearDown 来为每一个测试创建、销毁对象;
- TestSuite 类:是测试套件类,包含若干个 TestCase ,用于对测试进行分组管理;
- TestLoader 类:在编写测试中通常不会直接用,一些基于 unittest 工具可能会用到;
- TestResult 类:在编写测试中通常不会直接用,一些基于 unittest 工具可能会用到;
- main 方法:用于启动当前文件中的测试;
测试过程中,我们需要对程序的结果进行判断,来验证结果是否符合我们的预期及属性断言。
例如 assert 关键字就是断言的一种,不过 assert 关键字的功能比较弱,所以 TestCase 提供了多种强大的断言方法,如 assertTrue、assertFalse、assertEqual、assertNotEqual、assertIs等,参考文档见https://docs.python.org/3/library/unittest.html。这些断言方法可以在断言的同时加上一个 message 参数,这样可以使断言的意义明确而且方便维护,在测试失败时抛出可读的信息。
unittest 主要步骤
- 首先
import unittest
包; - 然后继承 unittest 包中的
TestCase
类,创建测试用例; - 接着定义
setUp
和tearDown
方法,在每个测试用例前后做一些辅助处理; - 之后开始正式定义测试用例,所有测试用例名字都是以 test 开头;
- 需要注意的是,单元测试和集成测试不一样,一个测试用例应该只测试一个方面,测试目的和测试内容都应该很明确。主要是调用
assertEqual
、assertRaises
等断言方法,来判断程序执行结果是否符合预期值; - 启动测试以后,调用
unittest.main()
来启动测试; - 运行测试,如果测试未通过,会输出相应的错误提示;如果测试全部通过则显示 ok,添加
-v
参数可以显示更多的详细信息。
Python 单元测试之 mock
Python 3.3 开始内置了 Mock 框架,可以使用 Mock 对象替代掉指定的 Python 对象,控制依赖对象的行为,并且记录对象是如何被调用的。
组成
它主要有以下几部分组成:
- Mock类:用于创建 Mock 对象,当访问 Mock 对象 的某个属性时,Mock 对象会自动创建该属性,并且为该属性也创建一个 Mock。
- MagicMock类:是 Mock 对象的子类,区别是它额外定义了操作符,比如大小比较、长度等等。
- patch装饰器:可以将其作用在测试方法上,用来限定在当前测试方法中使用 Mock 来替换真实对象,有些用 with 关键字创建一个作用域。
属性断言
Mock 对象提供了一些断言方法,用来我们断言对 Mock 项的调用,可从下面文档查看具体的断言方法说明:https://docs.python.org/3/library/unittest.mock.html#the-mock-class。
行为控制
我们通常需要从依赖对象的方法上得到返回值,Mock 对象也提供了一些途径来对返回值进行控制:
- return_value:固定返回值
- side_effects:返回值的序列或自定义方法
Python 单元测试之覆盖分析
Python 单元测试覆盖工具 coverage.py 可以通过 pip 来安装,它可以用来记录程序中每行语何的执行情况,也就是说可以处理覆盖率,它使用起来非常简单,并且支持最终生成界面友好的 HTML 报告。
覆盖率工具在 PyCharm 上中可以很方便地使用,在运行测试的时候选择和覆盖率一起来运行,就可以在 PyCharm 项目中看到覆盖率的结果。其中包括语句的覆盖情况,红色表示没有执行,绿色表示执行。还以看到语句覆盖率的统计。
案例:生命游戏
以生命游戏为案例,来学习一下 Python 单元测试。
生命游戏它包含四个文件,分别是主程序、输出程序、定时器还有生命游戏。通过 pylint 可以得到以下的依赖关系图:
可以看到 game_timer 还有 game_map 这两个是最底层的操作,在这两个模块当中game_map 又是最核心的模块,它包含了生命游戏的主要逻辑,有如下的属性方法:
接下来首先来创建测试,打开 game_map.py ,在菜单上选择「Navigate → Test」:
这时会在 game_map 类上弹出一个小菜单,选择「Create New Test」:
然后弹出创建测试的对话框:
在对话框中选择了要测试的方法(这里选择所有),然后输入测试文件:
点击 OK 后就完成了测试类的创建,PyCharm 会自动打开测试文件,可以看到 Test game_map 这个类确实继承了 testCase 类:
创建测试之后,在 Test game_map 类的类名上点击右键,选择「Run ‘Unittests in TestGameMap’」,运行测试:
然后我们就能够在 PyCharm 项目中看到测试结果,这里一共有 9 个测试:
我们看到失败 9 个,这是因为新创建的测试方法它默认调用了 TestCase 的私有方法,而这个方法会无条件地使单元测试失败。
下面我们来开始正式编写测试,首先来创建 fixture,回顾下:
- setUp 方法可以用来每个测试都需要的公共对象;
- tearDown 方法用来销毁公共对象,比如数据库断开连接、关闭文件等等;
这里我们只需要 setUp 方法,在其中创建一个 game_map 待测对象,简单起见,这里创建一个四行三列的 game_map:
然后我们对于 rows 和 cols 进行测试,通过 asserEqual 来判断行为 4 列为 3:
然后我们重新运行测试,可以看到这两个测试通过了:
接下来我们来看一下 get 和 set 方法,这两个方法有密切联系,我们把它合并到一个测试当中,这里首先我们断言默认的情况下,每个格子的值应该都是 0,然后我们给 0、0 格子设置为 1,断言 get 应该返回我们通过 set 设置的值:
再运行测试,通过:
然后是 rest 方法,这个方法它依赖于概率,所以需要我们进行 mock,这个方法的代码它对于每一个行每一列的每一个格子,它都会生成一个随机数,然后判断是否小于预先给定一个概率值,所以小于的话就把这个格子设成 1,否则设为 0
通过 patch 方式器,在 TestReset 方法中把 random 模块中的 random 方法换为 mock 对象,这个 mock 对象我们让它的行为是返回一个 0.3、0.6、0.9的循环序列,然后调用 reset 方法,接下来断言第一列全部为 1,后两列全部为 0:
然后测试,通过:
接下来是 get_neighbor_count 方法,图中看到需要访问 cell 属性:
这里可以对它进行设置,首先讲 cell 全部设为 1,这样所有格子的邻居数量都应该是 8,发现测试失败:
对于第一个格子,它邻居数量返回到 4 而不是 8。我们检查一下 get_neighbor_count 方法,由于 cell 全为 1,所以最后的结果只和循环次数有关,这样的话循环次数只跟 DIRECTIONS 有关,检查一下 DIRECTIONS,发现这里只写了四个相邻的方向,忽略了角的方向,就错了。把 DIRECTIONS 修改为正确的值,重新运行测试,通过:
get_neighbor_count_map 依赖 get_neighbor_count 方法,测试时,要对依赖方法进行 mock,以保持测试的独立性,这样 get_neighbor_count_map 的正确性就不依赖 get_neighbor_count 的正确性了。同时我们之后看概率的时候,也不会相互之间有干扰。
set_map 方法它本身比较简单,所以我们这主要测试一下它对于参数的检查是否完备:
这里我们主要使用了 assertRaises 来判断一个方法是否抛出了异常,我们看测试通过:
我们看到 print_map 调用了 print,所以我们还是可以通过为 print 进行 mock 来测试:
但是这样写本身是不好的,作为一个底层库这里就要访问一个字符串,我们先设定 cells,然后对 print 函数进行 mock,print 函数 builtins 包底下,然后我们调用了 assert has-calls 断言,这里的 call 是 mock 包里的一个代表函数调用的 mock,它的参数表示调用参数,然后我们运行测试发现通过:
到此我门所有的测试都通过了,在所有测试通过后,可以进行测试覆盖率分析。运行下测试覆盖率分析,可以看到 84% 的行都被覆盖了:
查看代码发现,没有覆盖的行,全都是这样的类型检查,事实上这些检查可以通过注解后,完全通过静态分析工具来检查,所以这里不再进行测试。
通常大家有一个误区,就是追求100%的覆盖率,但是100%的覆盖率并不能说明非常的正确,所以也不要得到「覆盖率 = 正确率」这样的错误结论。
其他工具
Python 还有很多其他的测试单元工具:
- 其他测试框架:
- python nose: https://nose.readthedocs.org/en/latest/
- pytest: http://pytest.org/latest/
- 其他 Mock 框架
- Python 测试工具大全(几乎包括了全部 Python 值的测试框架、Mock 框架、Web 测试工具、GUI测试工具等)