课件

3.4 单元测试工具.pdf

Python 单元测试之 unittest

Python 测试框架 unittest 是受到 unit 启发产生的,并且内置在 Python 中,无需安装。unittest 框架主要由以下几部分组成:
image.png

  • 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 主要步骤

  1. 首先 import unittest 包;
  2. 然后继承 unittest 包中的 TestCase 类,创建测试用例;
  3. 接着定义 setUptearDown 方法,在每个测试用例前后做一些辅助处理;
  4. 之后开始正式定义测试用例,所有测试用例名字都是以 test 开头;
  5. 需要注意的是,单元测试和集成测试不一样,一个测试用例应该只测试一个方面,测试目的和测试内容都应该很明确。主要是调用 assertEqualassertRaises 等断言方法,来判断程序执行结果是否符合预期值;
  6. 启动测试以后,调用 unittest.main() 来启动测试;
  7. 运行测试,如果测试未通过,会输出相应的错误提示;如果测试全部通过则显示 ok,添加 -v 参数可以显示更多的详细信息。

Python 单元测试之 mock

Python 3.3 开始内置了 Mock 框架,可以使用 Mock 对象替代掉指定的 Python 对象,控制依赖对象的行为,并且记录对象是如何被调用的。

组成

image.png
它主要有以下几部分组成:

  • 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 报告。
image.png
覆盖率工具在 PyCharm 上中可以很方便地使用,在运行测试的时候选择和覆盖率一起来运行,就可以在 PyCharm 项目中看到覆盖率的结果。其中包括语句的覆盖情况,红色表示没有执行,绿色表示执行。还以看到语句覆盖率的统计。

案例:生命游戏

以生命游戏为案例,来学习一下 Python 单元测试。

生命游戏它包含四个文件,分别是主程序、输出程序、定时器还有生命游戏。通过 pylint 可以得到以下的依赖关系图:
image.png
可以看到 game_timer 还有 game_map 这两个是最底层的操作,在这两个模块当中game_map 又是最核心的模块,它包含了生命游戏的主要逻辑,有如下的属性方法:
image.png

接下来首先来创建测试,打开 game_map.py ,在菜单上选择「Navigate → Test」:
image.png

这时会在 game_map 类上弹出一个小菜单,选择「Create New Test」:
image.png

然后弹出创建测试的对话框:
image.png

在对话框中选择了要测试的方法(这里选择所有),然后输入测试文件:
image.png

点击 OK 后就完成了测试类的创建,PyCharm 会自动打开测试文件,可以看到 Test game_map 这个类确实继承了 testCase 类:
image.png

创建测试之后,在 Test game_map 类的类名上点击右键,选择「Run ‘Unittests in TestGameMap’」,运行测试:
image.png

然后我们就能够在 PyCharm 项目中看到测试结果,这里一共有 9 个测试:
image.png
我们看到失败 9 个,这是因为新创建的测试方法它默认调用了 TestCase 的私有方法,而这个方法会无条件地使单元测试失败。

下面我们来开始正式编写测试,首先来创建 fixture,回顾下:

  • setUp 方法可以用来每个测试都需要的公共对象;
  • tearDown 方法用来销毁公共对象,比如数据库断开连接、关闭文件等等;

这里我们只需要 setUp 方法,在其中创建一个 game_map 待测对象,简单起见,这里创建一个四行三列的 game_map:
image.png

然后我们对于 rows 和 cols 进行测试,通过 asserEqual 来判断行为 4 列为 3:
image.png

然后我们重新运行测试,可以看到这两个测试通过了:
image.png

接下来我们来看一下 get 和 set 方法,这两个方法有密切联系,我们把它合并到一个测试当中,这里首先我们断言默认的情况下,每个格子的值应该都是 0,然后我们给 0、0 格子设置为 1,断言 get 应该返回我们通过 set 设置的值:
image.png

再运行测试,通过:
image.png

然后是 rest 方法,这个方法它依赖于概率,所以需要我们进行 mock,这个方法的代码它对于每一个行每一列的每一个格子,它都会生成一个随机数,然后判断是否小于预先给定一个概率值,所以小于的话就把这个格子设成 1,否则设为 0
image.png
通过 patch 方式器,在 TestReset 方法中把 random 模块中的 random 方法换为 mock 对象,这个 mock 对象我们让它的行为是返回一个 0.3、0.6、0.9的循环序列,然后调用 reset 方法,接下来断言第一列全部为 1,后两列全部为 0:
image.png

然后测试,通过:
image.png

接下来是 get_neighbor_count 方法,图中看到需要访问 cell 属性:
image.png
这里可以对它进行设置,首先讲 cell 全部设为 1,这样所有格子的邻居数量都应该是 8,发现测试失败:
image.png

对于第一个格子,它邻居数量返回到 4 而不是 8。我们检查一下 get_neighbor_count 方法,由于 cell 全为 1,所以最后的结果只和循环次数有关,这样的话循环次数只跟 DIRECTIONS 有关,检查一下 DIRECTIONS,发现这里只写了四个相邻的方向,忽略了角的方向,就错了。把 DIRECTIONS 修改为正确的值,重新运行测试,通过:
image.png

get_neighbor_count_map 依赖 get_neighbor_count 方法,测试时,要对依赖方法进行 mock,以保持测试的独立性,这样 get_neighbor_count_map 的正确性就不依赖 get_neighbor_count 的正确性了。同时我们之后看概率的时候,也不会相互之间有干扰。
image.png

set_map 方法它本身比较简单,所以我们这主要测试一下它对于参数的检查是否完备:
image.png

这里我们主要使用了 assertRaises 来判断一个方法是否抛出了异常,我们看测试通过:
image.png
image.png

我们看到 print_map 调用了 print,所以我们还是可以通过为 print 进行 mock 来测试:
image.png
但是这样写本身是不好的,作为一个底层库这里就要访问一个字符串,我们先设定 cells,然后对 print 函数进行 mock,print 函数 builtins 包底下,然后我们调用了 assert has-calls 断言,这里的 call 是 mock 包里的一个代表函数调用的 mock,它的参数表示调用参数,然后我们运行测试发现通过:
image.png

到此我门所有的测试都通过了,在所有测试通过后,可以进行测试覆盖率分析。运行下测试覆盖率分析,可以看到 84% 的行都被覆盖了:
image.png
查看代码发现,没有覆盖的行,全都是这样的类型检查,事实上这些检查可以通过注解后,完全通过静态分析工具来检查,所以这里不再进行测试。

通常大家有一个误区,就是追求100%的覆盖率,但是100%的覆盖率并不能说明非常的正确,所以也不要得到「覆盖率 = 正确率」这样的错误结论。

其他工具

Python 还有很多其他的测试单元工具: