Try it out

The Patchers in Python

Mock在单元测试中随处可见。在Python的单元测试中,常见的有unittest和pytest。unittest对应的Mock库是unittest.mock,pytest对应的是pytest-mock。这两个Mock库中都存在Patchers,Patchers与Mock密不可分

什么是Patch

Pytest-mock本质上与unittest.mock是相同的API,因此这里我们先从unittest.mock说起

A simple example

图1. Example code
图1. Example code

当测试目标是BubbleTeaShopmake_milk_tea方法,但又不想实际调用Kitchenmake_milk_tea方法时,就可以使用下图中的@patch()方法对class进行patch,并使用instance.make_milk_tea.return_value指定了方法的返回值

图2. 使用patch进行mock
图2. 使用patch进行mock

上例bubbleteashop.py中的Kitchen@patch所替换,@patch所返回的是一个unittst.mock中的MagicMock对象

图3. 被MagicMock替换
图3. 被MagicMock替换

Where to Patch


在官方文档中讲述Patch使用之前提到

Note: The key is to do the patching in the right namespace. See the section where to patch.

The basic principle is that you patch where an object is looked up, which is not necessarily the same place as where it is defined.

即在object被查找(被使用)的地方进行patch,而非定义的地方。并且文档也提到一个前提:object是通过from import导入的

以上例来讲,我们通过from src.kitchen import Kitchen的方式导入Kitchen,

所以patch的是src.bubbleteashop.Kitchen,而非src.kictchen.Kitchen

原因都与python的赋值机制直接相关

图4. 引用[Ned Batchelder - Python Names and Values](https://nedbatchelder.com/text/names1.html)

图4. 引用[Ned Batchelder - Python Names and Values](https://nedbatchelder.com/text/names1.html)

Ned Batchelder在Pycon 2015中分享时提到,Python 中的变量是引用值的名称。如果分配第二个名称,这些名称不会相互引用,它们都引用相同的值(x与y都指向了23)。如果然后再次分配其中一个名称(x=12),则另一个名称不受影响(y仍然是23)

在之前的例子中,from src.kitchen import Kitchen就等同于先导入src.kitchen,再进行赋值kitchen = src.kitchen.Kitchen,kitchen如同y,src.kitchen.Kitchen如同x。

如果以src.bubbleteashop.Kitchen进行patch时,bubbleteashop.pyKitchen会被替换为MagicMock,这正是测试中所希望达到的目标

如果以src.kictchen.Kitchen进行patch时,kitchen.pyKitchen会被替换为MagicMock,而bubbleteashop.pyKitchen不受影响,并没有被mock

图5. “from import”导入
图5. “from import”导入

如果我们以import src.kitchen进行导入呢?

图6. “import src.kitchen” 示例
图6. “import src.kitchen” 示例

import src.kitchen等同于在bubbleteashop.py中使src.kitchen指代整个kitchen模块,访问src.kitchen.make_milk_tea会进入到kitchen中,找到make_milk_tea,并执行它。

如果以src.kictchen.Kitchen进行patch时,kitchen.pyKitchen会被替换为MagicMock,因为bubbleteashop.pysrc.kitchen指代整个kitchen模块,所以mock同样会影响到bubbleteashop.py

图7. “import”导入
图7. “import”导入

Patch scope


Patch主要有三种方式,每一种方式的scope不同,使用场景也不同

图8. Patch scope
图8. Patch scope

Context manager

使用with的声明进行patch,patch的范围仅限于with语句的语句体

图9. Context manager scope
图9. Context manager scope

Function decorator

如同上例中创建在测试方法上的@patch(‘src.bubbleteashop.Kitchen’)就是一个function decorator,仅作用于当前测试方法

图10. Function decorator scope
图10. Function decorator scope

Class decorator

同样采用@pathch()的方法进行创建,不过是创建在class上,作用于这个test class的每一个方法,适用于多个测试方法共享同样的patcher

图11. Class decorator scope
图11. Class decorator scope