
CPython unittest.mock 实战指南从 Mock 基础到 Patch 高级用法的官方示例全解【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython本文以 CPython 仓库中随标准库发布的官方入门文档Doc/library/unittest.mock-examples.rst为主体完整梳理unittest.mock的三类核心能力——Mock对象的配置与断言、patch系列装饰器/上下文管理器的使用模式以及链式调用、可变参数、导入替换等进阶场景的官方示例。文中所有代码示例均可直接复制运行并结合Lib/unittest/mock.py的实现源码约 3200 行印证了关键行为背后的机制读完可以独立编写可维护、可验证的 Python 单元测试。背景与 mock 的类体系unittest.mock自 Python 3.3 起纳入标准库见文档头部的versionadded标记其实现位于 Lib/unittest/mock.py对应的完整 API 参考文档是 Doc/library/unittest.mock.rst本文聚焦的“getting started”示例文档则是 Doc/library/unittest.mock-examples.rst。从源码结构看mock 的类继承体系如下以 Lib/unittest/mock.py 等处的类定义为准类定义位置作用NonCallableMockmock.py#L433不可调用 mock 的基类提供调用记录、spec检查等基础设施Mockmock.py#L1263可调用 mockMock属性是MockMagicMockmock.py#L2223额外支持__len__、__iter__、__getitem__等魔术方法的 mock属性也是MagicMockAsyncMockmock.py#L24743.8 起用于 mock 协程支持await与异步上下文管理官方示例文档给出了一条重要提示在大多数示例中Mock与MagicMock可以互换使用由于MagicMock能力更强默认选用它是更稳妥的选择。使用 Mock替换方法与记录调用Mock对象的两大常见用途是替换patch方法与记录方法调用。替换对象上的方法当你想替换对象上的某个方法以检查它是否被系统的另一部分以正确参数调用时可以这样做real SomeClass() real.method MagicMock(namemethod) real.method(3, 4, 5, keyvalue)Mock 一旦被使用就会拥有用于断言其使用方式的方法和属性。调用后mock.called属性会被置为True更重要的是可以用assert_called_with或assert_called_once_with检查它是否以正确参数被调用。下面的例子测试调用ProductionClass().method会导致对something方法的调用class ProductionClass: def method(self): self.something(1, 2, 3) def something(self, a, b, c): pass real ProductionClass() real.something MagicMock() real.method() real.something.assert_called_once_with(1, 2, 3)Mock 对象上的方法调用另一种常见用法是把一个对象传入被测方法然后检查它是否被正确使用。class ProductionClass: def closer(self, something): something.close() real ProductionClass() mock Mock() real.closer(mock) mock.close.assert_called_with()这里不需要为 mock 提供close方法——访问close属性本身就会创建它。但如果close从未被真正调用过在测试中访问它虽然会创建子 mock随后assert_called_with仍会抛出失败异常这正是 mock“惰性创建 调用记账”机制带来的严格性。Mock 类通过 return_value 配置实例被 mock 掉的类会被替换为一个 mock而“实例”是通过调用类创建的因此要访问 mock 实例需要查看被 mock 类的返回值def some_function(): instance module.Foo() return instance.method() with patch(module.Foo) as mock: instance mock.return_value instance.method.return_value the result result some_function() assert result the result给 mock 命名给 mock 命名会让它的repr在测试失败信息中更可读并且名称会传播到子属性/子方法上mock MagicMock(namefoo) # MagicMock namefoo id... mock.method # MagicMock namefoo.method id...跟踪所有调用mock_calls 与 call 对象mock.mock_calls记录对 mock 所有子属性及其子级的调用。用call对象构造期望列表再比较mock MagicMock() mock.method() mock.attribute.method(10, x53) expected [call.method(), call.attribute.method(10, x53)] mock.mock_calls expected # True对mock_calls做断言的价值在于它不仅断言期望的调用发生过还隐式验证了调用顺序正确且没有多余调用。但要注意一个陷阱对“返回 mock 的调用”的参数不会被记录因此无法跟踪嵌套调用中创建祖先所用的参数m Mock() m.factory(importantTrue).deliver() m.mock_calls[-1] call.factory(importantFalse).deliver() # True参数丢失设置返回值与属性设置返回值极其简单且构造函数参数、属性直接赋值同样有效mock Mock() mock.return_value 3 mock() # 3 mock.method.return_value 3 mock.method() # 3 Mock(return_value3)() # 3 mock.x 3 # 任意属性直接赋值对更复杂的场景例如mock.connection.cursor().execute(SELECT 1)需要配置嵌套调用的结果用call.call_list()可以把链式调用对象拆成调用列表以便断言mock Mock() cursor mock.connection.cursor.return_value cursor.execute.return_value [foo] mock.connection.cursor().execute(SELECT 1) # [foo] expected call.connection.cursor().execute(SELECT 1).call_list() mock.mock_calls expected # Trueside_effect抛异常、可迭代与函数side_effect是Mock最有用的属性之一支持三种取值异常类或实例——mock 被调用时抛出该异常mock Mock(side_effectException(Boom!)) mock() # 抛出 Exception: Boom!可迭代对象——每次调用返回下一个值适合 mock 会被调用多次且每次返回不同值的场景mock MagicMock(side_effect[4, 5, 6]) mock() # 4 mock() # 5 mock() # 6函数——函数以与 mock 完全相同的参数被调用其返回值即为 mock 的返回值可用于根据参数动态决定返回什么vals {(1, 2): 1, (2, 3): 2} def side_effect(*args): return vals[args] mock MagicMock(side_effectside_effect) mock(1, 2) # 1 mock(2, 3) # 2Mock 异步迭代器与异步上下文管理器自 Python 3.8 起AsyncMock与MagicMock支持通过__aiter__mock 异步迭代器通过__aenter__/__aexit__mock 异步上下文管理器。__aiter__的return_value用于设置迭代产生的值mock MagicMock() # AsyncMock 同样适用 mock.__aiter__.return_value [1, 2, 3] async def main(): return [i async for i in mock] asyncio.run(main()) # [1, 2, 3]异步上下文管理器的__aenter__和__aexit__默认是AsyncMock实例class AsyncContextManager: async def __aenter__(self): return self async def __aexit__(self, exc_type, exc, tb): pass mock_instance MagicMock(AsyncContextManager()) # AsyncMock 同样适用 async def main(): async with mock_instance as result: pass asyncio.run(main()) mock_instance.__aenter__.assert_awaited_once() mock_instance.__aexit__.assert_awaited_once()从源码看这一“魔术方法自动选对 mock 类型”的行为由 mock.py 中的_get_child_mock实现当父 mock 是MagicMock且子 mock 名称落在_async_method_magics中时子 mock 会被创建为AsyncMock反之AsyncMock上的同步方法会被创建为MagicMock。从已有对象创建 mockspec 与 spec_set过度使用 mock 的隐患是测试与 mock 的实现耦合而非与真实代码耦合——重构掉某个方法后测试依然会绿。spec关键字参数解决了这个问题以对象或类作为 mock 的规范访问规范对象上不存在的属性会立即抛出AttributeErrormock Mock(specSomeClass) mock.old_method() # AttributeError: Mock object has no attribute old_method使用 spec 还带来“更聪明的调用匹配”无论参数是按位置还是按关键字传入都能正确比对def f(a, b, c): pass mock Mock(specf) mock(1, 2, 3) mock.assert_called_with(a1, b2, c3) # 成功若想让方法调用也享受这种匹配可以使用自动 specauto-speccing。spec_set是更严格的形式既禁止读取不存在的方法也禁止设置任意属性。用 mock_open 按文件返回不同内容mock_open用于 patchopen结合side_effect函数可以为每次调用返回一个“全新”的 file mock从而实现按文件名返回不同内容from unittest.mock import mock_open, patch DEFAULT default data_dict {file1: data1, file2: data2} def open_side_effect(name): return mock_open(read_datadata_dict.get(name, DEFAULT))() with patch(builtins.open, side_effectopen_side_effect): with open(file1) as file1: assert file1.read() data1 with open(file2) as file2: assert file2.read() data2 with open(file3) as file2: assert file2.read() defaultmock_open的实现位于 mock.py#L2962其返回的 file mock 本身仍是完整 mock因此read、迭代等调用都会被记录、可断言。Patch 装饰器patch、patch.object、patch.dict使用patch时必须注意要在对象被查找的命名空间中替换它详见官方文档“where to patch”一节。模块和类在效果上是全局的因此对它们的 patch 必须在测试结束后撤销否则会泄漏到其他测试。mock 为此提供三个便捷装饰器patch(package.module.Class.attribute, new)以点分字符串定位属性可指定替换值patch.object(obj, name, new)直接传对象与属性名patch.dict(dict, values, clearFalse)在一个作用域内修改字典包括模块字典、sys.modules结束后还原。original SomeClass.attribute patch.object(SomeClass, attribute, sentinel.attribute) def test(): assert SomeClass.attribute sentinel.attribute test() assert SomeClass.attribute original # 已还原patch 一个模块包括builtins时用patch而非patch.objectmock MagicMock(return_valuesentinel.file_handle) with patch(builtins.open, mock): handle open(filename, r) mock.assert_called_with(filename, r) assert handle sentinel.file_handle模块名可以是点分形式package.module、package.module.ClassName.attribute。装饰测试方法本身是推荐模式class MyTest(unittest.TestCase): patch.object(SomeClass, attribute, sentinel.attribute) def test_something(self): self.assertEqual(SomeClass.attribute, sentinel.attribute)当只提供 patch 目标patch一个参数 /patch.object两个参数时mock 会被自动创建并作为额外参数传入测试函数/方法class MyTest(unittest.TestCase): patch.object(SomeClass, static_method) def test_something(self, mock_method): SomeClass.static_method() mock_method.assert_called_with()多个 patch 装饰器可以叠加。注意传参顺序mock 按装饰器从下往上应用Python 装饰器的常规顺序因此下面例子中ClassName2的 mock 先传入class MyTest(unittest.TestCase): patch(package.module.ClassName1) patch(package.module.ClassName2) def test_something(self, MockClass2, MockClass1): self.assertIs(package.module.ClassName1, MockClass1) self.assertIs(package.module.ClassName2, MockClass2)patch.dict用于字典作用域修改支持clearTrue先清空原内容foo {key: value} with patch.dict(foo, {newkey: newvalue}, clearTrue): assert foo {newkey: newvalue} assert foo {key: value} # 自动还原patch、patch.object、patch.dict三者都可以作为上下文管理器使用其中自动创建 mock 的场景用with ... as mock取到引用with patch.object(ProductionClass, method) as mock_method: real ProductionClass() real.method(1, 2, 3) mock_method.assert_called_with(1, 2, 3)它们同样可以作为类装饰器使用等价于把装饰器分别应用到每个以test开头的方法上。从源码看这一行为在_patch.decorate_class中实现遍历类属性凡以patch.TEST_PREFIX即test开头的可调用属性都会绑定该 patcher 的一份拷贝patcher self.copy()从而保证每个测试方法各自独立 start/stop互不串扰。进阶示例Mock 链式调用一旦理解return_value链式调用 mock 就很直接mock 首次被调用或提前取return_value时会新建一个子Mock因此可以通过审问return_value这个 mock 来查看“返回值”被如何使用mock Mock() mock().foo(a2, b3) mock.return_value.foo.assert_called_with(a2, b3)以真实代码为例Something.method()中有一长串链式调用class Something: def __init__(self): self.backend BackendProvider() def method(self): response self.backend.get_endpoint(foobar).create_call(spam, eggs).start_call() # more code假设只关心最终start_call的返回对象可以用configure_mock以“点分路径”一步配置多层return_value避免冗长的链式赋值something Something() mock_response Mock(specopen) # 以 open 为 spec 的 file-like 对象 mock_backend Mock() config {get_endpoint.return_value.create_call.return_value.start_call.return_value: mock_response} mock_backend.configure_mock(**config) something.backend mock_backend something.method() # 一条断言验证整条链 chained call.get_endpoint(foobar).create_call(spam, eggs).start_call() assert mock_backend.mock_calls chained.call_list()链式调用在代码中是一行但会产生多条mock_calls记录call.call_list()恰好能把一个call对象展开为对应的调用列表。configure_mock的实现位于 mock.py#L668。部分 mockpatch date 但保留真实构造datetime.date用 C 实现无法直接 monkey-patch 其静态方法today。官方给出的方案是把被测模块里的date整体替换为 mock再通过side_effect把构造调用转发给真实的dt.date——date.today()返回固定日期而date(...)仍创建真实日期对象import datetime as dt with patch(mymodule.date) as mock_date: mock_date.today.return_value dt.date(2010, 10, 8) mock_date.side_effect lambda *args, **kw: dt.date(*args, **kw) assert mymodule.date.today() dt.date(2010, 10, 8) assert mymodule.date(2009, 6, 8) dt.date(2009, 6, 8)注意这里是 patch 使用方的模块mymodule.date而非全局datetime.date。构造调用也会被记入mock_date的call_count等属性。避免部分 mock 的替代做法是重写被测代码使其依赖注入日期这也是官方文档指出的“用同被测代码的算法手算期望值”这一经典反模式的解法之一。Mock 生成器方法生成器函数被调用时返回生成器对象真正的迭代发生在__iter__协议上因此只需配置iter方法的return_value为一个真实迭代器class Foo: def iter(self): for i in [1, 2, 3]: yield i mock_foo MagicMock() mock_foo.iter.return_value iter([1, 2, 3]) list(mock_foo.iter()) # [1, 2, 3]让每个测试方法共享同一组 patch三种方式由繁到简类装饰器patch应用于类自动作用于所有以test开头的方法not_a_test不受影响patch(mymodule.SomeClass) class MyTest(unittest.TestCase): def test_one(self, MockSomeClass): self.assertIs(mymodule.SomeClass, MockSomeClass)setUp/tearDown 管理 start/stop但要注意若setUp抛异常tearDown不会被调用stop 会漏掉。用addCleanup更稳class MyTest(unittest.TestCase): def setUp(self): patcher patch(mymodule.foo) self.addCleanup(patcher.stop) self.mock_foo patcher.start() def test_foo(self): self.assertIs(mymodule.foo, self.mock_foo)辅助方法消除嵌套缩进当多个 patch 叠成深层with时用create_patchaddCleanup拉平结构“嵌套 patch”一节详述。Mock 未绑定方法autospecTrue 的妙用直接用一个Mock替换类上的未绑定方法是有问题的从实例取到的 mock 不会变成绑定方法也就拿不到self。patch(..., autospecTrue)解决此问题——它用一个真实函数对象做替换该函数拥有与被替换函数相同的签名内部再委托给 mockclass Foo: def foo(self): pass with patch.object(Foo, foo, autospecTrue) as mock_foo: mock_foo.return_value foo foo Foo() foo.foo() # foo mock_foo.assert_called_once_with(foo) # 断言时记得带上 self从源码看这正是_setup_func所为autospec 创建的是一个funcopy带__signature__的真实函数把assert_called_with等断言方法与side_effect桥接到内部 mock 上因为它是一个普通函数Python 的描述符机制会自然地在实例属性访问时把它绑定self便作为第一个参数传入。不使用autospecTrue时未绑定方法被换成普通Mock实例不会被调用时带上self。检查多次调用assert_called_with与assert_called_once_with都只针对最近一次调用assert_called_once_with还会断言call_count恰好为 1多调一次即失败mock.foo_bar(baz, spameggs) mock.foo_bar.assert_called_once_with(baz, spameggs) mock.foo_bar() mock.foo_bar.assert_called_once_with(baz, spameggs) # AssertionError: Expected foo_bar to be called once. Called 2 times.要对所有调用断言使用call_args_list配合call构造期望列表mock(1, 2, 3); mock(4, 5, 6); mock() mock.call_args_list # [call(1, 2, 3), call(4, 5, 6), call()] expected [call(1, 2, 3), call(4, 5, 6), call()] mock.call_args_list expected # True应对可变参数调用时快照call_args/call_args_list保存的是参数的引用——若被测代码随后修改了可变参数事后断言就会失真# mymodule 中 def frob(val): pass def grob(val): frob(val) val.clear() with patch(mymodule.frob) as mock_frob: val {6} mymodule.grob(val) mock_frob.assert_called_with({6}) # AssertionError: Expected: (({6},), {}) Called with: ((set(),), {})官方给出的解法是用side_effect函数在调用发生的瞬间深拷贝参数并转存到另一个 mock且side_effect返回DEFAULT以保留原 mock 的正常返回值from copy import deepcopy from unittest.mock import Mock, patch, DEFAULT def copy_call_args(mock): new_mock Mock() def side_effect(*args, **kwargs): args deepcopy(args) kwargs deepcopy(kwargs) new_mock(*args, **kwargs) return DEFAULT mock.side_effect side_effect return new_mock with patch(mymodule.frob) as mock_frob: new_mock copy_call_args(mock_frob) val {6} mymodule.grob(val) new_mock.assert_called_with({6}) new_mock.call_args # call({6})如果 mock 只会用一次更简单的做法是直接在side_effect里断言另一种更系统的做法是子类化Mock/MagicMock在__call__中深拷贝参数class CopyingMock(MagicMock): def __call__(self, /, *args, **kwargs): args deepcopy(args) kwargs deepcopy(kwargs) return super().__call__(*args, **kwargs) c CopyingMock(return_valueNone) arg set() c(arg) arg.add(1) c.assert_called_with(set()) # 成功 c.assert_called_with(arg) # 失败实际记录的是快照 set()由 mock 的子类派生出的动态属性和return_value会自动沿用同一子类因此c.foo也是CopyingMock。嵌套 patch 的拉平写法多层with patch(...)会使缩进不断右移。借助addCleanup与 start/stop 可消除嵌套class MyTest(unittest.TestCase): def create_patch(self, name): patcher patch(name) thing patcher.start() self.addCleanup(patcher.stop) return thing def test_foo(self): mock_foo self.create_patch(mymodule.Foo) mock_bar self.create_patch(mymodule.Bar) mock_spam self.create_patch(mymodule.Spam) assert mymodule.Foo is mock_foo # ...测试结束后mymodule.Foo已还原为原对象。用 MagicMock mock 字典想要“像字典一样工作、同时记录所有访问”的 mock用MagicMock配合side_effect把读写委托给真实字典即可访问不存在的键会真实抛出KeyErrormy_dict {a: 1, b: 2, c: 3} def getitem(name): return my_dict[name] def setitem(name, val): my_dict[name] val mock MagicMock() mock.__getitem__.side_effect getitem mock.__setitem__.side_effect setitem mock[a] # 1 mock[d] # KeyError: d mock[b] fish mock[__getitem__.join([]) or b] # fish mock.__getitem__.call_args_list # [call(a), call(c), call(d), call(b), ...]官方文档还给出两个替代方案其一用Mock而只显式提供需要的魔术方法mock.__getitem__ Mock(side_effectgetitem)行为更收敛其二MagicMock(spec_setdict)让 mock 只暴露字典相关的魔术方法。Mock 子类与 _get_child_mock给Mock子类添加辅助方法是常见诉求。默认行为是属性与return_value产生的子 mock 与父 mock 同类型因此子类上的辅助方法会自动传播到所有子 mockclass MyMock(MagicMock): def has_been_called(self): return self.called mymock MyMock(return_valueNone) mymock.foo # MyMock namemock.foo id...同样有 has_been_called若不希望子 mock 继承子类例如某些适配器在属性上出现子类实例会引发错误可以重写_get_child_mock——它接收**kwargs并转发给 mock 构造函数class Subclass(MagicMock): def _get_child_mock(self, /, **kwargs): return MagicMock(**kwargs) mymock Subclass() assert not isinstance(mymock.foo, Subclass) assert not isinstance(mymock(), Subclass)一个例外是不可调用的 mock其属性会采用可调用变体否则不可调用 mock 将永远无法拥有可调用方法对应源码 mock.py#L1088-L1092 中的分支。用 patch.dict 替换 sys.modules 来 mock 导入函数内部的局部 import 难以通过常规 patch 处理因为 import 是从sys.modules字典中取对象而该对象不必是模块。patch.dict可以在测试作用域内往sys.modules临时塞入 mock结束自动还原import sys mock Mock() with patch.dict(sys.modules, {fooble: mock}): import fooble fooble.blob() assert fooble not in sys.modules # 作用域结束后已还原 mock.blob.assert_called_once_with()from fooble import blob形式同样可行mock 包导入只需同时放入package与package.module两个键modules {package: mock, package.module: mock.module} with patch.dict(sys.modules, modules): from package.module import fooble fooble() mock.module.fooble.assert_called_once_with()官方文档也提醒局部 import 通常应避免防循环依赖应重构延迟导入可以用“类/模块属性 首次使用时导入”替代mock 导入是最后手段。跨多个 mock 的调用顺序跟踪method_calls只能跟踪单个 mock 上的方法顺序。想要跨对象跟踪可以制造一个“manager”父 mock让各被测对象从它派生——由于访问任意属性都会创建子 mock子 mock 的调用会按顺序记入父 mock 的mock_callsmanager Mock() mock_foo manager.foo mock_bar manager.bar mock_foo.something() mock_bar.other.thing() manager.mock_calls # [call.foo.something(), call.bar.other.thing()] expected_calls [call.foo.something(), call.bar.other.thing()] manager.mock_calls expected_calls # True若 mock 是由patch创建的可用attach_mock把它们挂到 manager 上实现见 mock.py#L508manager MagicMock() with patch(mymodule.Class1) as MockClass1: with patch(mymodule.Class2) as MockClass2: manager.attach_mock(MockClass1, MockClass1) manager.attach_mock(MockClass2, MockClass2) MockClass1().foo() MockClass2().bar() manager.mock_calls # [call.MockClass1(), call.MockClass1().foo(), call.MockClass2(), call.MockClass2().bar()]如果只关心某个子序列用assert_has_calls只要该序列按序出现在mock_calls中即可不要求是唯一调用再加any_orderTrue则可忽略顺序m MagicMock() m().foo().bar().baz() m.one().two().three() m.assert_has_calls(call.one().two().three().call_list()) m(1); m.two(2, 3); m.seven(7); m.fifty(50) m.assert_has_calls([call.fifty(50), call(1), call.seven(7)], any_orderTrue)更复杂的参数匹配自定义 Matcher当期望传入的参数是一个按身份比较的自定义对象而你只关心其中几个属性时可以为assert_called_with提供自定义Matcher利用断言库内部用比较实参与期望参数的机制让Matcher.__eq__调用自定义比较函数class Foo: def __init__(self, a, b): self.a, self.b a, b mock Mock(return_valueNone) mock(Foo(1, 2)) mock.assert_called_with(Foo(1, 2)) # AssertionError两个 Foo 对象按身份不相等 def compare(self, other): if type(self) ! type(other): return False if self.a ! other.a: return False if self.b ! other.b: return False return True class Matcher: def __init__(self, compare, some_obj): self.compare compare self.some_obj some_obj def __eq__(self, other): return self.compare(self.some_obj, other) mock.assert_called_with(Matcher(compare, Foo(1, 2))) # 成功 mock.assert_called_with(Matcher(compare, Foo(3, 4))) # AssertionError小技巧让比较函数直接抛出带更友好消息的AssertionError失败输出会更有诊断价值。官方示例文档指出ANY定义于 mock.py#L2529就是同一思想的内置版本社区库 PyHamcrest 也提供同类的 equality matcher。测试与延伸阅读unittest.mock的官方测试套件位于 Lib/test/test_unittest/testmock/testmock.py覆盖了TestPatch、autospec、异步 mock 等本文涉及的各场景是验证上述行为最权威的“活文档”socket 相关的 mock 辅助则见 Lib/test/mock_socket.py。配套阅读路径示例总入口本文主体Doc/library/unittest.mock-examples.rst完整 API 参考Doc/library/unittest.mock.rst实现源码Lib/unittest/mock.py模块包结构Lib/unittest/掌握本文内容后你应能覆盖日常测试中绝大多数替身需求用MagicMock与side_effect控制行为、用spec/autospec防止过度 mock、用patch系列在正确命名空间安全替换并自动还原以及用mock_calls/call_list/assert_has_calls精确断言调用的参数、次数与顺序。【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考