Python unittest.mock 的基本使用

平时随意用 Python 写的用了就丢的小工具当然没必要写什么单元测试,如果是一个要反复打磨的工具,项目,单元测试就必须重视起来了。因为简单思路写出来的代码将要应对各种未知情形,加之想要大肆的重构,有了足够的测试用例才能安心。

任何语言白盒的单元测试首先面对的就是 Mock,  不光是应对有副作用的操作,最好是隔离所有不在当前所见到的代码的任何调用,其实像 print 这种有副作用的代码一般是能容忍的,可能也是为何很多测试框架默认会把控制台的输出关掉。

在 Python 中,一般基本的东西自己都有,Mock 就直接用 unittest.mock,可应用于所有的单元测试框架中,如 unittest, pytest。另有一块对 uniitest.mock 的简单封装,专为 pytest 提供方便的 pytest-mock。今天先了解 unittest.mock 的使用,它的官方文档是 unittest.mock -- mock object library

unittest.mock 常见的是类 Mock 和 MagicMock, 还有方法(也用作装饰器) patch()。MagicMock(MagicMock(MagicMixin, Mock):) 是增强型的 Mock, 如果愿意的话,可以用 MagicMock  g一撸到底。unittest.mock 的功能和其他语言的 Mock 框架一样,可用来 Mock 方法调用和断言属性,调用次数,参数。Python 的 Mock 可比其他静态语言强大的多,能 Mock 的对象几乎不受限,什么类方法,静态方法,还有 Python 自带库的方法,而且使用起来特别灵活。这大概是得益于 Python 的元编程或可随时动态添加方法和属性的特点。

通过 Mock 或  MagicMock 可以任意(多层次)的添加属性或方法,只要没用 return_value, side_effect, 直接赋值等反回的仍然是一个 Mock。

下面进入到各种实际应用场景

Mock 外部方法调用

下面的代码,比如 repo.get_data() 需要从数据库中查询数据

测试时需要 Mock 住 repo.get_data() 调用,这时我们的测试用例就可以写成

Mock 有副作用外部操作(用到 patch)

比如一个函数进行了文件删除操作,我们当然不想在单元测试中有这样的破坏性操作,这时候要用到 patch。待测试方法如下

单元测试代码

以上代码可以安全的执行,因为 shutil.rmtree 不会进行实际的操作。如果会删除文件的话基本上测试是不成功的,因为文件系统中大约是没有 "world" 这个目录的。

patch("os.getevn").start() 返回的是一个 MagicMock 类型

patch 用了 @contextlib.contextmanager 注解,所以可以写成  with context manager 的形式,上

这时候就不需要 start() 了

patch 还是一个装饰器,所以又有另一种形式,通过装饰器注入函数参数

注意 @patch 时方法参数的顺序是相反的,先写的 @patch 声明为最后的参数。Python 的解释是离方法名最近的装饰器最先读取作为第一个方法参数。如果调个位置,写成

测试就会失败了,因为上面的 mock_rmtree 实际上是 patch("os.getenv").start() 产生的值。

@patch 还能同时带上 return_value

也可 @patch "os", 但必须加上 import os 所在的模块名,例如在 demo.py 中的

测试时就用

Mock 异常

Mock 异常只需要设置 Mock 对象的 side_effect 为一个异常对象即可。比如一个待测试类

想要测试在调用 foo() 时,如果其中的 bar() 抛出异常的情况, 我们可以写下面的测试

通过这个例子,同时也实现了 Java 中 Mock 对象中部分方法时类似 spy 的功能,即这里的 bar() 被 Mock 了, foo() 还是调用原本的方法。

Mock 特定参数的方法调用

前面用 side_effect 模拟了抛出异常,它同时也可以定制 Mock 方法不同参数时产生不一样的输出,side_effect 可以接受一个 lambda 或函数作为参数。同样是测试

希望 Mock os.getenv() 输入 KEY1,KEY2,和其他时获得不一样的环境变量值,用下面的测试代码

但是如何在别的 key 是调用实际的 os.getenv() 方法呢?我们可以先把原始方法在被 Mock 之前保存下来,然后参考下面的方式

Mock 构造函数

在 Python 中,构造函数可能更多指的是 __new__ 函数,但也可能连带提到 __init__ 初始函数,而我们在用 MyClass() 创建对象时会调用到前两个函数。Mock 构造函数在 Python 中比 Java 等其他语言简单多了,因为 MyClass  本身就可以看作是一个 callable 函数,只要 Mock 它就行了。

比如,要 Mock MyClass.__init__ 中的 Cat(), 使用代码

unittest.mock 比 Java 的 Mock 框架灵活多了,完全没有构造方法,静态方法,类方法等约束,把它们当作普通方法来 Mock 就行。

Mock dunder 方法也没问题,如上一个测试,加上这样的代码行

所以 Mock __init__ 方法也能尝试一下,从上面的代码来看 Mock dunder 那样的双下划线方法不一定得要 MagicMock, Mock 就能胜任。

patch.object 临时修改某对象的属性或方法

我们无需每次都从最初的函数下手,或对已有 Mock 对象动手脚,针对已有实际对象可用 patch.object(target, attribute,...), 如

对已创建的 my_class 实例,我们将只 Mock get_value 方法

即使在 path.object 上下文当中,get_value2() 也是调用实际的方法实现。

把上面第二三行改成如下方式也是一样的效果

使用 patch.object(target, attribute,...) 时 target 可以是任何 Python 对象,包括模块本身,类,实例对象,或都是函数, attribute 可以是属性或方法。比如应用到 Mock 前面的  os.getenv 方法

patch.object() 也可作为装饰器来用,如

patch.dict 临时修改 dict 的内容

一看  patch.dict 就应该是 patch.object 专门作用于 dict 快捷方法,看官方的例子

还有更多的 unittest.mock 使用方法,如 patch.multiple(...), mock_open, sentinel, 这些要实际应用时再查询,巩固。

pytest-mock 简介

如果项目中使用 pytest 编写和运行测试用例,建议用 pytest-mock 来 Mock 操作,pytest-mock 是对  unittest.mock 的封装,所以它  100% 兼容后者,学习起来也不难。它主要用借助于 pytest_mock.plugin.MockerFixture 接口来使用,有 Mock 生命周期的自动清理功能,额外提供 spy(), stub(), autospec=True 的功能。

大概看一个比较完整的例子,其中把业务代码与测试代码写在了同一个文件 demo.py 中

再应用上 stub() 的话,可以把上面代码的 mock_get = mocker.path(...) 换成以下几行

会得到一样的效果。

相比于 unittest.mock 的 @patch 前后倒置添加方法参数的方式,pytest-mock 通过单一个参数 mocker: MockerFixture 用起来显得太简洁一些。

本文链接 https://yanbin.blog/python-unittest-mock-usages/, 来自 隔叶黄莺 Yanbin Blog

[版权声明] Creative Commons License 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。

guest

0 Comments
Inline Feedbacks
View all comments