Mock 的方法是解决单元测试等简单测试方法时过于隔离,非真是真实场景。在复杂系统下需要接入外部系统信息进行测试,例如接受外部数据、完整流程的系统测试,但是在开发阶段并接入外部依赖存在较大的复杂性以及增加了开发阶段测试的难度。因此通过 “模拟”的方式来解决以上的问题,在 Python 的 3.x 版本之后提供了 mock 的基础模块以解决 Python 开发过程中的上述问题。
1. Mock 对象 Mock 对象常用的方式包括了:1)补丁方法,2)对象方法或者属性调用。Mock 常用对象包括了 Mock
和 MagicMock
(此外还有其他 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 from unittest import mockm = mock.Mock(name="withoutspec" ) m.attr m.te() m.attr = 12 m.te = lambda x: x + 12
在 Mock 对象的属性,可以调整为方法即需要通过 m.attr()
的方式调用。该方式是直接在属性上配置 return_value
的属性值即可:
>folded 1 2 3 4 5 m.attr1.return_value = 12 m.attr1() m.attr1
除了配置一个 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 m.attr2.side_effect = range(12 ) m.attr3.side_effect = ValueError("Wrong Value" ) 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=obj) m = mock.Mock(spec_set=obj())
需要注意如果添加非对象的属性时,会提示属性错误。另外如果 spec
和 spec_set
参数,得到的数据值是一个具体的数据,那么该 Mock 对象会将 __class__
属性值修改为该对象的类。
>folded 1 2 3 4 5 m = mock.Mock() m.__class__ = dict isinstance(m, dict)
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 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
的方式进行伪造对象——例如模拟外部对象,这样可以实现全局进行访问而并非真实创建一个需要的对象。
参考
参考书籍 Leonardo Giordani, Clean Architectures In Python
unittest.mock <a