Mock 技巧

Mock 的方法是解决单元测试等简单测试方法时过于隔离,非真是真实场景。在复杂系统下需要接入外部系统信息进行测试,例如接受外部数据、完整流程的系统测试,但是在开发阶段并接入外部依赖存在较大的复杂性以及增加了开发阶段测试的难度。因此通过 “模拟”的方式来解决以上的问题,在 Python 的 3.x 版本之后提供了 mock 的基础模块以解决 Python 开发过程中的上述问题。

1. Mock 对象

Mock 对象常用的方式包括了:1)补丁方法,2)对象方法或者属性调用。Mock 常用对象包括了 MockMagicMock(此外还有其他 Mock 对象),通常情况下是可以互换使用的[^2],但后者有更丰富的属性方法而被广泛使用。初始化对象时常用的参数包括:

  • name: 用于申明对象名称,是一个可选项
  • spec: 用于申明一个对象,可以用于限制可用的对象或者方法。在没有指定该参数时, Mock 对象调用属性或者方法可以直接创建;反之,如果指定没有在 spec 中的会出现属性错误
  • return_value: 表示调用相关的方法时,返回的值
  • side_effect: 可以接收可调用对象可迭代对象异常以及根据情况的行为变化

1.1 Mock 对象创建及其基本应用

创建基本的 Mock 对象:

>folded
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# load package
from unittest import mock

# 创建没有 spec 的对象
m = mock.Mock(name="withoutspec") # 该对象 repr 示例<Mock name='withoutspec' id='139745029751952'>

# 调用对象任意属性或者方法,如果不存在时会直接创建相关属性和方法
m.attr # 该方式会直接返回 <Mock name='withoutspec.attr' id='139745029763472'> 并且添加 attr 属性

m.te() # 该方式会直接返回 <Mock name='withoutspec.te()' id='139745029777488'> 并且添加 te 方法

# 修改属性或者方法,直接使用赋值方式
m.attr = 12 # 该方式不会返回任何结果,但是属性值会被调整

m.te = lambda x: x + 12 # 修改 te 的方法,可以通过 m.te(x=23) 的方式调用

在 Mock 对象的属性,可以调整为方法即需要通过 m.attr() 的方式调用。该方式是直接在属性上配置 return_value 的属性值即可:

>folded
1
2
3
4
5
m.attr1.return_value = 12

# 要返回 attr1 的值,需要通过方法的方式调用
m.attr1() # 返回值是 12
m.attr1 # 会返回该属性的信息,<Mock name='withoutspec.att1' id='139745733653712'>

除了配置一个 return_value 的方式创建一个方法,可以通过 side_effect 属性进行配置。side_effect 的可配置功能更丰富,例如需要配置一个可迭代对象 m.attr2.side_effect = range(12),即可以创建一个 iterator 对象。如果是需要直接 mock 一个函数,可以以 t = mock.Mock(return_value=12) 创建一个 t() 的函数。

>folded
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 配置 iterator
m.attr2.side_effect = range(12) # 可以被 next() 函数调用

# 配置 exception
m.attr3.side_effect = ValueError("Wrong Value") # 以 m.attr3() 调用时会出现异常


# 配置对象
class obj:
def __init__(self):
self.attr1 = 12
self.attr2 = None

def method(self, arg):
pass
m.attr4.side_effect = obj

使用 side_effect 方式只是相当于将类赋值给类 attr4,但要创建对象还是需要初始化赋值一次。这里存在另一种应用条件,即直接将类指定给 Mock 对象,保留相应对象的属性和方法——可以在初始化时通过 spec 或者 spec_set 参数配置。

>folded
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class obj:
attr1 = None
attr2 = None
def __init__(self):
self.attr3 = 12
self.attr4 = None

def method(self, arg):
pass

# 等效于 m = mock.Mock(spec_set=obj),这两种方式下都表示 m 可以调整 attr1, attr2 属性
m = mock.Mock(spec=obj)

# 等效于 m = mock.Mock(spec_set=obj()),这两种方式下可以调整 attr1, attr2, attr3, attr4
m = mock.Mock(spec_set=obj())

需要注意如果添加非对象的属性时,会提示属性错误。另外如果 specspec_set 参数,得到的数据值是一个具体的数据,那么该 Mock 对象会将 __class__ 属性值修改为该对象的类。

>folded
1
2
3
4
5
m = mock.Mock()
# 等效于 m = mock.Mock(spec={})
m.__class__ = dict

isinstance(m, dict) # 结果是 True

1.2 断言调用-asserting calls

对于外部命令需要检验其是否被执行、类方法是否被调用等,测试过程中将相应的命令或者方法进行 mock 之后在调用 assert_call* 等方法,即可进行测试:

>unfolded
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# 创建类
class Elimination:
def __init__(self, values, *, rows=None, cols=None, copy=True) -> None:
if rows is None and cols is None:
rows = math.sqrt(len(values))
cols = rows
elif rows is not None and cols is not None:
pass
else:
miss = "rows" if rows is None else "cols"
raise Exception(f"Arguments '{miss}' lost")

# 创建环境
rows, cols, grid = int(rows), int(cols), int(math.sqrt(rows))
self.env = Board(rows, cols, grid)

if copy:
self._copy = self._copy_env(self.env)


@classmethod
def _copy_env(cls, board):
result = Board(board.rows, board.cols)
for cell, target in zip(result, board):
if isinstance(target.value, int):
cell.value = str(target.value)
elif isinstance(target.value, str):
if target.value.isdigit():
cell.value = target.value
elif target.value == "" or target.value == ".":
cell.value = []
else:
raise ValueError(f"Cell value is not a digit value,"
f" get type {type(target.value)}")

return result

# 使用 mock 测试
class TestElimination(unittest.TestCase):
def setUp(self) -> None:
return super().setUp()

def test_copy_env_classmethod(self):
# 这里进行测试
elimination.Elimination._copy_env = mock.MagicMock(name='copy')
elim = elimination.Elimination("1134223")
elim._copy_env.assert_called_once()

Patch 测试

Mock 可以拥有模拟外源数据,对于硬编码无法模拟出外源数据点情况可以通过 patching 的方式进行伪造对象——例如模拟外部对象,这样可以实现全局进行访问而并非真实创建一个需要的对象。

参考

  1. 参考书籍 Leonardo Giordani, Clean Architectures In Python
  2. unittest.mock <a
作者

ZenRay

发布于

2020-12-09

更新于

2021-02-19

许可协议

CC BY-NC-SA 4.0