“Python/docs/3.9/library/unittest.mock-examples”的版本间差异
(autoload) |
小 (Page commit) |
||
第1行: | 第1行: | ||
+ | {{DISPLAYTITLE:unittest.mock — 入门 — Python 文档}} | ||
<div id="unittest-mock-getting-started" class="section"> | <div id="unittest-mock-getting-started" class="section"> | ||
− | = | + | = unittest.mock — 入门 = |
<div class="versionadded"> | <div class="versionadded"> | ||
− | <span class="versionmodified added">3.3 | + | <span class="versionmodified added">3.3 版中的新功能。</span> |
第12行: | 第13行: | ||
<div id="using-mock" class="section"> | <div id="using-mock" class="section"> | ||
− | == | + | == 使用模拟 == |
<div id="mock-patching-methods" class="section"> | <div id="mock-patching-methods" class="section"> | ||
− | === | + | === 模拟修补方法 === |
− | + | [[../unittest.mock#unittest.mock|Mock]] 对象的常见用途包括: | |
− | * | + | * 修补方法 |
− | * | + | * 记录对象的方法调用 |
− | + | 您可能想要替换对象上的方法以检查系统的另一部分是否使用正确的参数调用它: | |
− | |||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第30行: | 第30行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> real = SomeClass() |
− | + | >>> real.method = MagicMock(name='method') | |
− | + | >>> real.method(3, 4, 5, key='value') | |
− | + | <MagicMock name='method()' id='...'></syntaxhighlight> | |
</div> | </div> | ||
</div> | </div> | ||
− | + | 一旦使用了我们的模拟(在本例中为 <code>real.method</code>),它就会有一些方法和属性,可以让你断言它是如何被使用的。 | |
− | |||
<div class="admonition note"> | <div class="admonition note"> | ||
− | + | 笔记 | |
− | + | 在大多数这些示例中,[[../unittest.mock#unittest.mock|Mock]] 和 [[../unittest.mock#unittest.mock|MagicMock]] 类是可以互换的。 由于 <code>MagicMock</code> 是功能更强大的类,因此默认情况下使用它是一个明智的选择。 | |
− | |||
− | |||
</div> | </div> | ||
− | + | 一旦模拟被调用,它的 [[../unittest.mock#unittest.mock.Mock|called]] 属性被设置为 <code>True</code>。 更重要的是,我们可以使用 [[../unittest.mock#unittest.mock.Mock|assert_called_with()]] 或 [[../unittest.mock#unittest.mock.Mock|assert_Called_once_with()]] 方法来检查它是否使用正确的参数被调用。 | |
− | <code>True</code> | ||
− | [[../unittest.mock#unittest.mock.Mock| | ||
− | |||
− | + | 此示例测试调用 <code>ProductionClass().method</code> 会导致调用 <code>something</code> 方法: | |
− | |||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第63行: | 第56行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> class ProductionClass: |
... def method(self): | ... def method(self): | ||
... self.something(1, 2, 3) | ... self.something(1, 2, 3) | ||
第69行: | 第62行: | ||
... pass | ... pass | ||
... | ... | ||
− | + | >>> real = ProductionClass() | |
− | + | >>> real.something = MagicMock() | |
− | + | >>> real.method() | |
− | + | >>> real.something.assert_called_once_with(1, 2, 3)</syntaxhighlight> | |
</div> | </div> | ||
第81行: | 第74行: | ||
<div id="mock-for-method-calls-on-an-object" class="section"> | <div id="mock-for-method-calls-on-an-object" class="section"> | ||
− | === | + | === 模拟对象上的方法调用 === |
− | + | 在上一个示例中,我们直接在对象上修补了一个方法以检查它是否被正确调用。 另一个常见的用例是将一个对象传递给一个方法(或被测系统的某个部分),然后检查它是否以正确的方式使用。 | |
− | |||
− | |||
− | |||
− | + | 下面简单的<code>ProductionClass</code>有一个<code>closer</code>方法。 如果它被一个对象调用,那么它会在它上面调用 <code>close</code>。 | |
− | |||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第95行: | 第84行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> class ProductionClass: |
... def closer(self, something): | ... def closer(self, something): | ||
... something.close() | ... something.close() | ||
− | ...</ | + | ...</syntaxhighlight> |
</div> | </div> | ||
</div> | </div> | ||
− | + | 所以为了测试它,我们需要使用 <code>close</code> 方法传入一个对象并检查它是否被正确调用。 | |
− | |||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第110行: | 第98行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> real = ProductionClass() |
− | + | >>> mock = Mock() | |
− | + | >>> real.closer(mock) | |
− | + | >>> mock.close.assert_called_with()</syntaxhighlight> | |
</div> | </div> | ||
</div> | </div> | ||
− | + | 我们不需要做任何工作来在我们的模拟上提供 'close' 方法。 访问 close 创建它。 因此,如果 'close' 尚未被调用,则在测试中访问它会创建它,但 [[../unittest.mock#unittest.mock.Mock|assert_Called_with()]] 将引发失败异常。 | |
− | |||
− | |||
− | |||
第127行: | 第112行: | ||
<div id="mocking-classes" class="section"> | <div id="mocking-classes" class="section"> | ||
− | === | + | === 模拟类 === |
− | + | 一个常见的用例是模拟由您的被测代码实例化的类。 当您修补一个类时,该类将被替换为模拟。 实例是通过 ''调用类'' 创建的。 这意味着您可以通过查看模拟类的返回值来访问“模拟实例”。 | |
− | |||
− | |||
− | |||
− | + | 在下面的例子中,我们有一个函数 <code>some_function</code> 实例化 <code>Foo</code> 并调用它的方法。 对 [[../unittest.mock#unittest.mock|patch()]] 的调用用模拟替换了类 <code>Foo</code>。 <code>Foo</code>实例是调用mock的结果,所以通过修改mock[[../unittest.mock#unittest.mock.Mock|return_value]]来配置。 | |
− | |||
− | |||
− | |||
<div class="highlight-python3 notranslate"> | <div class="highlight-python3 notranslate"> | ||
第143行: | 第122行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python3">>>> def some_function(): |
... instance = module.Foo() | ... instance = module.Foo() | ||
... return instance.method() | ... return instance.method() | ||
... | ... | ||
− | + | >>> with patch('module.Foo') as mock: | |
... instance = mock.return_value | ... instance = mock.return_value | ||
... instance.method.return_value = 'the result' | ... instance.method.return_value = 'the result' | ||
... result = some_function() | ... result = some_function() | ||
− | ... assert result == 'the result'</ | + | ... assert result == 'the result'</syntaxhighlight> |
</div> | </div> | ||
第160行: | 第139行: | ||
<div id="naming-your-mocks" class="section"> | <div id="naming-your-mocks" class="section"> | ||
− | === | + | === 命名你的模拟 === |
− | + | 给你的模拟一个名字会很有用。 该名称显示在模拟的 repr 中,当模拟出现在测试失败消息中时会很有帮助。 该名称还传播到模拟的属性或方法: | |
− | |||
− | |||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第170行: | 第147行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> mock = MagicMock(name='foo') |
− | + | >>> mock | |
− | + | <MagicMock name='foo' id='...'> | |
− | + | >>> mock.method | |
− | + | <MagicMock name='foo.method' id='...'></syntaxhighlight> | |
</div> | </div> | ||
第183行: | 第160行: | ||
<div id="tracking-all-calls" class="section"> | <div id="tracking-all-calls" class="section"> | ||
− | === | + | === 跟踪所有呼叫 === |
− | + | 通常,您希望跟踪对方法的多次调用。 [[../unittest.mock#unittest.mock.Mock|mock_calls]] 属性记录了对模拟的子属性的所有调用——以及对它们的子属性的调用。 | |
− | [[../unittest.mock#unittest.mock.Mock| | ||
− | |||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第193行: | 第168行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> mock = MagicMock() |
− | + | >>> mock.method() | |
− | + | <MagicMock name='mock.method()' id='...'> | |
− | + | >>> mock.attribute.method(10, x=53) | |
− | + | <MagicMock name='mock.attribute.method()' id='...'> | |
− | + | >>> mock.mock_calls | |
− | [call.method(), call.attribute.method(10, x=53)]</ | + | [call.method(), call.attribute.method(10, x=53)]</syntaxhighlight> |
</div> | </div> | ||
</div> | </div> | ||
− | + | 如果您对 <code>mock_calls</code> 进行断言并且调用了任何意外方法,则该断言将失败。 这很有用,因为除了断言您期望的调用已经进行之外,您还可以检查它们是否以正确的顺序进行并且没有额外的调用: | |
− | |||
− | |||
− | |||
− | + | 您使用 [[../unittest.mock#unittest.mock|call]] 对象来构造列表以与 <code>mock_calls</code> 进行比较: | |
− | <code>mock_calls</code> | ||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第216行: | 第187行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> expected = [call.method(), call.attribute.method(10, x=53)] |
− | + | >>> mock.mock_calls == expected | |
− | True</ | + | True</syntaxhighlight> |
</div> | </div> | ||
</div> | </div> | ||
− | + | 但是,不会记录返回模拟的调用的参数,这意味着无法跟踪用于创建祖先的参数很重要的嵌套调用: | |
− | |||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第230行: | 第200行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> m = Mock() |
− | + | >>> m.factory(important=True).deliver() | |
− | + | <Mock name='mock.factory().deliver()' id='...'> | |
− | + | >>> m.mock_calls[-1] == call.factory(important=False).deliver() | |
− | True</ | + | True</syntaxhighlight> |
</div> | </div> | ||
第243行: | 第213行: | ||
<div id="setting-return-values-and-attributes" class="section"> | <div id="setting-return-values-and-attributes" class="section"> | ||
− | === | + | === 设置返回值和属性 === |
− | + | 在模拟对象上设置返回值非常简单: | |
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第251行: | 第221行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> mock = Mock() |
− | + | >>> mock.return_value = 3 | |
− | + | >>> mock() | |
− | 3</ | + | 3</syntaxhighlight> |
</div> | </div> | ||
</div> | </div> | ||
− | + | 当然,您可以对模拟上的方法执行相同的操作: | |
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第265行: | 第235行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> mock = Mock() |
− | + | >>> mock.method.return_value = 3 | |
− | + | >>> mock.method() | |
− | 3</ | + | 3</syntaxhighlight> |
</div> | </div> | ||
</div> | </div> | ||
− | + | 返回值也可以在构造函数中设置: | |
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第279行: | 第249行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> mock = Mock(return_value=3) |
− | + | >>> mock() | |
− | 3</ | + | 3</syntaxhighlight> |
</div> | </div> | ||
</div> | </div> | ||
− | + | 如果您需要在模拟上设置属性,请执行以下操作: | |
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第292行: | 第262行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> mock = Mock() |
− | + | >>> mock.x = 3 | |
− | + | >>> mock.x | |
− | 3</ | + | 3</syntaxhighlight> |
</div> | </div> | ||
</div> | </div> | ||
− | + | 有时您想模拟更复杂的情况,例如 <code>mock.connection.cursor().execute("SELECT 1")</code>。 如果我们希望这个调用返回一个列表,那么我们必须配置嵌套调用的结果。 | |
− | <code>mock.connection.cursor().execute("SELECT 1")</code> | ||
− | |||
− | + | 我们可以使用 [[../unittest.mock#unittest.mock|call]] 在像这样的“链式调用”中构造调用集,以便之后轻松断言: | |
− | |||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第311行: | 第278行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> mock = Mock() |
− | + | >>> cursor = mock.connection.cursor.return_value | |
− | + | >>> cursor.execute.return_value = ['foo'] | |
− | + | >>> mock.connection.cursor().execute("SELECT 1") | |
['foo'] | ['foo'] | ||
− | + | >>> expected = call.connection.cursor().execute("SELECT 1").call_list() | |
− | + | >>> mock.mock_calls | |
[call.connection.cursor(), call.connection.cursor().execute('SELECT 1')] | [call.connection.cursor(), call.connection.cursor().execute('SELECT 1')] | ||
− | + | >>> mock.mock_calls == expected | |
− | True</ | + | True</syntaxhighlight> |
</div> | </div> | ||
</div> | </div> | ||
− | + | 正是对 <code>.call_list()</code> 的调用将我们的调用对象转换为代表链接调用的调用列表。 | |
− | |||
第332行: | 第298行: | ||
<div id="raising-exceptions-with-mocks" class="section"> | <div id="raising-exceptions-with-mocks" class="section"> | ||
− | === | + | === 使用模拟引发异常 === |
− | + | 一个有用的属性是 [[../unittest.mock#unittest.mock.Mock|side_effect]]。 如果将此设置为异常类或实例,则在调用模拟时将引发异常。 | |
− | |||
− | |||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第342行: | 第306行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> mock = Mock(side_effect=Exception('Boom!')) |
− | + | >>> mock() | |
Traceback (most recent call last): | Traceback (most recent call last): | ||
... | ... | ||
− | Exception: Boom!</ | + | Exception: Boom!</syntaxhighlight> |
</div> | </div> | ||
第355行: | 第319行: | ||
<div id="side-effect-functions-and-iterables" class="section"> | <div id="side-effect-functions-and-iterables" class="section"> | ||
− | === | + | === 副作用函数和迭代 === |
− | <code>side_effect</code> | + | <code>side_effect</code> 也可以设置为一个函数或一个可迭代对象。 <code>side_effect</code> 作为可迭代对象的用例是您的模拟将被多次调用,并且您希望每次调用都返回不同的值。 当您将 <code>side_effect</code> 设置为可迭代对象时,每次调用模拟都会返回可迭代对象的下一个值: |
− | <code>side_effect</code> | ||
− | |||
− | <code>side_effect</code> | ||
− | |||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第367行: | 第327行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> mock = MagicMock(side_effect=[4, 5, 6]) |
− | + | >>> mock() | |
4 | 4 | ||
− | + | >>> mock() | |
5 | 5 | ||
− | + | >>> mock() | |
− | 6</ | + | 6</syntaxhighlight> |
</div> | </div> | ||
</div> | </div> | ||
− | + | 对于更高级的用例,例如根据调用模拟的内容动态改变返回值,<code>side_effect</code> 可以是一个函数。 将使用与模拟相同的参数调用该函数。 无论函数返回什么,调用都会返回什么: | |
− | |||
− | |||
− | |||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第387行: | 第344行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> vals = {(1, 2): 1, (2, 3): 2} |
− | + | >>> def side_effect(*args): | |
... return vals[args] | ... return vals[args] | ||
... | ... | ||
− | + | >>> mock = MagicMock(side_effect=side_effect) | |
− | + | >>> mock(1, 2) | |
1 | 1 | ||
− | + | >>> mock(2, 3) | |
− | 2</ | + | 2</syntaxhighlight> |
</div> | </div> | ||
第404行: | 第361行: | ||
<div id="mocking-asynchronous-iterators" class="section"> | <div id="mocking-asynchronous-iterators" class="section"> | ||
− | === | + | === 模拟异步迭代器 === |
− | + | 从 Python 3.8 开始,<code>AsyncMock</code> 和 <code>MagicMock</code> 支持通过 <code>__aiter__</code> 模拟 [[../../reference/datamodel#async-iterators|Asynchronous Iterators]]。 <code>__aiter__</code> 的 [[../unittest.mock#unittest.mock.Mock|return_value]] 属性可用于设置要用于迭代的返回值。 | |
− | [[../../reference/datamodel#async-iterators| | ||
− | |||
− | |||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第415行: | 第369行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> mock = MagicMock() # AsyncMock also works here |
− | + | >>> mock.__aiter__.return_value = [1, 2, 3] | |
− | + | >>> async def main(): | |
... return [i async for i in mock] | ... return [i async for i in mock] | ||
... | ... | ||
− | + | >>> asyncio.run(main()) | |
− | [1, 2, 3]</ | + | [1, 2, 3]</syntaxhighlight> |
</div> | </div> | ||
第430行: | 第384行: | ||
<div id="mocking-asynchronous-context-manager" class="section"> | <div id="mocking-asynchronous-context-manager" class="section"> | ||
− | === | + | === 模拟异步上下文管理器 === |
− | + | 从 Python 3.8 开始,<code>AsyncMock</code> 和 <code>MagicMock</code> 支持通过 <code>__aenter__</code> 和 <code>__aexit__</code> 模拟 [[../../reference/datamodel#async-context-managers|异步上下文管理器]]。 默认情况下,<code>__aenter__</code> 和 <code>__aexit__</code> 是返回异步函数的 <code>AsyncMock</code> 实例。 | |
− | [[../../reference/datamodel#async-context-managers| | ||
− | |||
− | |||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第441行: | 第392行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> class AsyncContextManager: |
... async def __aenter__(self): | ... async def __aenter__(self): | ||
... return self | ... return self | ||
第447行: | 第398行: | ||
... pass | ... pass | ||
... | ... | ||
− | + | >>> mock_instance = MagicMock(AsyncContextManager()) # AsyncMock also works here | |
− | + | >>> async def main(): | |
... async with mock_instance as result: | ... async with mock_instance as result: | ||
... pass | ... pass | ||
... | ... | ||
− | + | >>> asyncio.run(main()) | |
− | + | >>> mock_instance.__aenter__.assert_awaited_once() | |
− | + | >>> mock_instance.__aexit__.assert_awaited_once()</syntaxhighlight> | |
</div> | </div> | ||
第463行: | 第414行: | ||
<div id="creating-a-mock-from-an-existing-object" class="section"> | <div id="creating-a-mock-from-an-existing-object" class="section"> | ||
− | === | + | === 从现有对象创建模拟 === |
− | + | 过度使用模拟的一个问题是它将您的测试与模拟的实现相结合,而不是您的真实代码。 假设您有一个实现 <code>some_method</code> 的类。 在另一个类的测试中,您提供了 ''也'' 提供 <code>some_method</code> 的此对象的模拟。 如果稍后您重构第一个类,使其不再具有 <code>some_method</code> - 那么即使您的代码现在已损坏,您的测试仍将继续通过! | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | [[../unittest.mock#unittest.mock| | + | [[../unittest.mock#unittest.mock|Mock]] 允许您提供一个对象作为模拟的规范,使用 ''spec'' 关键字参数。 访问您的规范对象上不存在的模拟上的方法/属性将立即引发属性错误。 如果您更改规范的实现,则使用该类的测试将立即开始失败,而无需在这些测试中实例化该类。 |
− | |||
− | |||
− | |||
− | |||
− | |||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第483行: | 第424行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> mock = Mock(spec=SomeClass) |
− | + | >>> mock.old_method() | |
Traceback (most recent call last): | Traceback (most recent call last): | ||
... | ... | ||
− | AttributeError: object has no attribute 'old_method'</ | + | AttributeError: object has no attribute 'old_method'</syntaxhighlight> |
</div> | </div> | ||
</div> | </div> | ||
− | + | 使用规范还可以更智能地匹配对模拟的调用,无论某些参数是作为位置参数还是命名参数传递的: | |
− | |||
− | |||
<div class="highlight-python3 notranslate"> | <div class="highlight-python3 notranslate"> | ||
第500行: | 第439行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python3">>>> def f(a, b, c): pass |
... | ... | ||
− | + | >>> mock = Mock(spec=f) | |
− | + | >>> mock(1, 2, 3) | |
− | + | <Mock name='mock()' id='140161580456576'> | |
− | + | >>> mock.assert_called_with(a=1, b=2, c=3)</syntaxhighlight> | |
</div> | </div> | ||
</div> | </div> | ||
− | + | 如果您希望这种更智能的匹配也适用于模拟上的方法调用,您可以使用 [[../unittest|auto-speccing]]。 | |
− | |||
− | + | 如果您想要一种更强大的规范形式来防止设置任意属性以及获取它们,那么您可以使用 ''spec_set'' 而不是 ''spec''。 | |
− | |||
− | ''spec_set'' | ||
第523行: | 第459行: | ||
<div id="patch-decorators" class="section"> | <div id="patch-decorators" class="section"> | ||
− | == | + | == 补丁装饰器 == |
<div class="admonition note"> | <div class="admonition note"> | ||
− | + | 笔记 | |
− | + | 使用 [[../unittest.mock#unittest.mock|patch()]] 重要的是您在查找它们的命名空间中修补对象。 这通常很简单,但要获得快速指南,请阅读 [[../unittest|在哪里打补丁]] 。 | |
− | |||
− | |||
</div> | </div> | ||
− | + | 测试中的一个常见需求是修补类属性或模块属性,例如修补内置模块或修补模块中的类以测试它是否已实例化。 模块和类实际上是全局的,因此必须在测试后取消对它们的修补,否则修补程序将持续存在于其他测试中并导致难以诊断的问题。 | |
− | |||
− | |||
− | |||
− | |||
− | mock | + | mock 为此提供了三个方便的装饰器:[[../unittest.mock#unittest.mock|patch()]]、[[../unittest.mock#unittest.mock.patch|patch.object()]] 和 [[../unittest.mock#unittest.mock.patch|patch.dict()]]。 <code>patch</code> 采用单个字符串,形式为 <code>package.module.Class.attribute</code> 来指定您正在修补的属性。 它还可选地采用您希望替换属性(或类或其他)的值。 'patch.object' 接受一个对象和你想要修补的属性的名称,加上可选的值来修补它。 |
− | [[../unittest.mock#unittest.mock.patch| | ||
− | <code>package.module.Class.attribute</code> | ||
− | |||
− | |||
− | |||
− | |||
− | <code>patch.object</code> | + | <code>patch.object</code>: |
<div class="highlight-python3 notranslate"> | <div class="highlight-python3 notranslate"> | ||
第555行: | 第479行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python3">>>> original = SomeClass.attribute |
− | + | >>> @patch.object(SomeClass, 'attribute', sentinel.attribute) | |
... def test(): | ... def test(): | ||
... assert SomeClass.attribute == sentinel.attribute | ... assert SomeClass.attribute == sentinel.attribute | ||
... | ... | ||
− | + | >>> test() | |
− | + | >>> assert SomeClass.attribute == original | |
− | + | >>> @patch('package.module.attribute', sentinel.attribute) | |
... def test(): | ... def test(): | ||
... from package.module import attribute | ... from package.module import attribute | ||
... assert attribute is sentinel.attribute | ... assert attribute is sentinel.attribute | ||
... | ... | ||
− | + | >>> test()</syntaxhighlight> | |
</div> | </div> | ||
</div> | </div> | ||
− | + | 如果您正在修补模块(包括 [[../builtins#module-builtins|builtins]]),则使用 [[../unittest.mock#unittest.mock|patch()]] 而不是 [[../unittest.mock#unittest.mock.patch|patch.object()]]: | |
− | |||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第580行: | 第503行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> mock = MagicMock(return_value=sentinel.file_handle) |
− | + | >>> with patch('builtins.open', mock): | |
... handle = open('filename', 'r') | ... handle = open('filename', 'r') | ||
... | ... | ||
− | + | >>> mock.assert_called_with('filename', 'r') | |
− | + | >>> assert handle == sentinel.file_handle, "incorrect file handle returned"</syntaxhighlight> | |
</div> | </div> | ||
</div> | </div> | ||
− | + | 如果需要,模块名称可以“带点”,格式为 <code>package.module</code>: | |
<div class="highlight-python3 notranslate"> | <div class="highlight-python3 notranslate"> | ||
第596行: | 第519行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python3">>>> @patch('package.module.ClassName.attribute', sentinel.attribute) |
... def test(): | ... def test(): | ||
... from package.module import ClassName | ... from package.module import ClassName | ||
... assert ClassName.attribute == sentinel.attribute | ... assert ClassName.attribute == sentinel.attribute | ||
... | ... | ||
− | + | >>> test()</syntaxhighlight> | |
</div> | </div> | ||
</div> | </div> | ||
− | + | 一个不错的模式是实际装饰测试方法本身: | |
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第612行: | 第535行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> class MyTest(unittest.TestCase): |
... @patch.object(SomeClass, 'attribute', sentinel.attribute) | ... @patch.object(SomeClass, 'attribute', sentinel.attribute) | ||
... def test_something(self): | ... def test_something(self): | ||
... self.assertEqual(SomeClass.attribute, sentinel.attribute) | ... self.assertEqual(SomeClass.attribute, sentinel.attribute) | ||
... | ... | ||
− | + | >>> original = SomeClass.attribute | |
− | + | >>> MyTest('test_something').test_something() | |
− | + | >>> assert SomeClass.attribute == original</syntaxhighlight> | |
</div> | </div> | ||
</div> | </div> | ||
− | + | 如果你想用一个 Mock 打补丁,你可以使用只有一个参数的 [[../unittest.mock#unittest.mock|patch()]] (或带有两个参数的 [[../unittest.mock#unittest.mock.patch|patch.object()]] )。 将为您创建模拟并传递给测试函数/方法: | |
− | |||
− | |||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第632行: | 第553行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> class MyTest(unittest.TestCase): |
... @patch.object(SomeClass, 'static_method') | ... @patch.object(SomeClass, 'static_method') | ||
... def test_something(self, mock_method): | ... def test_something(self, mock_method): | ||
第638行: | 第559行: | ||
... mock_method.assert_called_with() | ... mock_method.assert_called_with() | ||
... | ... | ||
− | + | >>> MyTest('test_something').test_something()</syntaxhighlight> | |
</div> | </div> | ||
</div> | </div> | ||
− | + | 您可以使用此模式堆叠多个补丁装饰器: | |
<div class="highlight-python3 notranslate"> | <div class="highlight-python3 notranslate"> | ||
第649行: | 第570行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python3">>>> class MyTest(unittest.TestCase): |
... @patch('package.module.ClassName1') | ... @patch('package.module.ClassName1') | ||
... @patch('package.module.ClassName2') | ... @patch('package.module.ClassName2') | ||
第656行: | 第577行: | ||
... self.assertIs(package.module.ClassName2, MockClass2) | ... self.assertIs(package.module.ClassName2, MockClass2) | ||
... | ... | ||
− | + | >>> MyTest('test_something').test_something()</syntaxhighlight> | |
</div> | </div> | ||
</div> | </div> | ||
− | + | 当您嵌套补丁装饰器时,模拟以它们应用的相同顺序传递给装饰函数(应用装饰器的正常 ''Python'' 顺序)。 这意味着自下而上,因此在上面的示例中,首先传入 <code>test_module.ClassName2</code> 的模拟。 | |
− | |||
− | |||
− | |||
− | + | 还有 [[../unittest.mock#unittest.mock.patch|patch.dict()]] 用于在范围内设置字典中的值,并在测试结束时将字典恢复到其原始状态: | |
− | |||
− | |||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第674行: | 第590行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> foo = {'key': 'value'} |
− | + | >>> original = foo.copy() | |
− | + | >>> with patch.dict(foo, {'newkey': 'newvalue'}, clear=True): | |
... assert foo == {'newkey': 'newvalue'} | ... assert foo == {'newkey': 'newvalue'} | ||
... | ... | ||
− | + | >>> assert foo == original</syntaxhighlight> | |
</div> | </div> | ||
</div> | </div> | ||
− | <code>patch</code> | + | <code>patch</code>、<code>patch.object</code> 和 <code>patch.dict</code> 都可以用作上下文管理器。 |
− | + | 在您使用 [[../unittest.mock#unittest.mock|patch()]] 为您创建模拟的地方,您可以使用 with 语句的“as”形式获取对模拟的引用: | |
− | |||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第693行: | 第608行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> class ProductionClass: |
... def method(self): | ... def method(self): | ||
... pass | ... pass | ||
... | ... | ||
− | + | >>> with patch.object(ProductionClass, 'method') as mock_method: | |
... mock_method.return_value = None | ... mock_method.return_value = None | ||
... real = ProductionClass() | ... real = ProductionClass() | ||
... real.method(1, 2, 3) | ... real.method(1, 2, 3) | ||
... | ... | ||
− | + | >>> mock_method.assert_called_with(1, 2, 3)</syntaxhighlight> | |
</div> | </div> | ||
</div> | </div> | ||
− | + | 作为替代 <code>patch</code>,<code>patch.object</code> 和 <code>patch.dict</code> 可以用作类装饰器。 以这种方式使用时,它与将装饰器单独应用于名称以“test”开头的每个方法相同。 | |
− | |||
− | |||
第716行: | 第629行: | ||
<span id="id1"></span> | <span id="id1"></span> | ||
− | == | + | == 进一步的例子 == |
− | + | 下面是一些稍微更高级的场景的更多示例。 | |
<div id="mocking-chained-calls" class="section"> | <div id="mocking-chained-calls" class="section"> | ||
− | === | + | === 模拟链式调用 === |
− | + | 一旦你理解了 [[../unittest.mock#unittest.mock.Mock|return_value]] 属性,模拟链调用实际上很简单。 当第一次调用模拟时,或者在调用之前获取它的 <code>return_value</code> 时,会创建一个新的 [[../unittest.mock#unittest.mock|Mock]]。 | |
− | |||
− | |||
− | |||
− | + | 这意味着您可以通过询问 <code>return_value</code> 模拟来了解从调用模拟对象返回的对象是如何被使用的: | |
− | |||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第736行: | 第645行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> mock = Mock() |
− | + | >>> mock().foo(a=2, b=3) | |
− | + | <Mock name='mock().foo()' id='...'> | |
− | + | >>> mock.return_value.foo.assert_called_with(a=2, b=3)</syntaxhighlight> | |
</div> | </div> | ||
</div> | </div> | ||
− | + | 从这里开始,这是一个简单的配置步骤,然后对链式调用进行断言。 当然,另一种选择是首先以更可测试的方式编写代码…… | |
− | |||
− | |||
− | + | 所以,假设我们有一些看起来像这样的代码: | |
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第754行: | 第661行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> class Something: |
... def __init__(self): | ... def __init__(self): | ||
... self.backend = BackendProvider() | ... self.backend = BackendProvider() | ||
... def method(self): | ... def method(self): | ||
... response = self.backend.get_endpoint('foobar').create_call('spam', 'eggs').start_call() | ... response = self.backend.get_endpoint('foobar').create_call('spam', 'eggs').start_call() | ||
− | ... # more code</ | + | ... # more code</syntaxhighlight> |
</div> | </div> | ||
</div> | </div> | ||
− | + | 假设<code>BackendProvider</code>已经测试好了,我们如何测试<code>method()</code>? 具体来说,我们要测试代码段 <code># more code</code> 是否以正确的方式使用响应对象。 | |
− | <code>method()</code> | ||
− | + | 由于此调用链是从实例属性进行的,我们可以在 <code>Something</code> 实例上对 <code>backend</code> 属性进行猴子修补。 在这种特殊情况下,我们只对最终调用 <code>start_call</code> 的返回值感兴趣,因此我们没有太多配置要做。 让我们假设它返回的对象是“类文件”,因此我们将确保我们的响应对象使用内置的 [[../functions#open|open()]] 作为其 <code>spec</code>。 | |
− | |||
− | |||
− | <code>start_call</code> | ||
− | |||
− | |||
− | + | 为此,我们创建一个模拟实例作为我们的模拟后端,并为其创建一个模拟响应对象。 要将响应设置为最终 <code>start_call</code> 的返回值,我们可以这样做: | |
− | |||
− | <code>start_call</code> | ||
<div class="highlight-python3 notranslate"> | <div class="highlight-python3 notranslate"> | ||
第782行: | 第681行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python3">mock_backend.get_endpoint.return_value.create_call.return_value.start_call.return_value = mock_response</syntaxhighlight> |
</div> | </div> | ||
</div> | </div> | ||
− | + | 我们可以使用 [[../unittest.mock#unittest.mock.Mock|configure_mock()]] 方法以更好的方式直接为我们设置返回值: | |
− | |||
<div class="highlight-python3 notranslate"> | <div class="highlight-python3 notranslate"> | ||
第794行: | 第692行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python3">>>> something = Something() |
− | + | >>> mock_response = Mock(spec=open) | |
− | + | >>> mock_backend = Mock() | |
− | + | >>> config = {'get_endpoint.return_value.create_call.return_value.start_call.return_value': mock_response} | |
− | + | >>> mock_backend.configure_mock(**config)</syntaxhighlight> | |
</div> | </div> | ||
</div> | </div> | ||
− | + | 有了这些,我们就可以修补“模拟后端”,并可以进行真正的调用: | |
− | |||
<div class="highlight-python3 notranslate"> | <div class="highlight-python3 notranslate"> | ||
第810行: | 第707行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python3">>>> something.backend = mock_backend |
− | + | >>> something.method()</syntaxhighlight> | |
</div> | </div> | ||
</div> | </div> | ||
− | + | 使用 [[../unittest.mock#unittest.mock.Mock|mock_calls]] 我们可以使用单个断言检查链接的调用。 一个链式调用是一行代码中的多个调用,所以在<code>mock_calls</code>中会有多个条目。 我们可以使用 [[../unittest.mock#unittest.mock.call|call.call_list()]] 为我们创建这个调用列表: | |
− | |||
− | |||
− | |||
<div class="highlight-python3 notranslate"> | <div class="highlight-python3 notranslate"> | ||
第825行: | 第719行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python3">>>> chained = call.get_endpoint('foobar').create_call('spam', 'eggs').start_call() |
− | + | >>> call_list = chained.call_list() | |
− | + | >>> assert mock_backend.mock_calls == call_list</syntaxhighlight> | |
</div> | </div> | ||
第836行: | 第730行: | ||
<div id="partial-mocking" class="section"> | <div id="partial-mocking" class="section"> | ||
− | === | + | === 部分嘲讽 === |
− | + | 在某些测试中,我想模拟对 [[../datetime#datetime.date|datetime.date.today()]] 的调用以返回已知日期,但我不想阻止被测代码创建新的日期对象。 不幸的是 [[../datetime#datetime|datetime.date]] 是用 C 编写的,所以我不能只是修补静态 <code>date.today()</code> 方法。 | |
− | |||
− | |||
− | |||
− | + | 我找到了一种简单的方法来做到这一点,该方法涉及用模拟有效地包装日期类,但将构造函数的调用传递给真正的类(并返回真实的实例)。 | |
− | |||
− | |||
− | + | [[../unittest.mock#unittest.mock|patch 装饰器]] 用于模拟被测模块中的 <code>date</code> 类。 然后将模拟日期类的 <code>side_effect</code> 属性设置为返回真实日期的 lambda 函数。 当模拟日期类被调用时,<code>side_effect</code> 将构造并返回一个真实日期。 | |
− | |||
− | |||
− | |||
− | |||
<div class="highlight-python3 notranslate"> | <div class="highlight-python3 notranslate"> | ||
第857行: | 第742行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python3">>>> from datetime import date |
− | + | >>> with patch('mymodule.date') as mock_date: | |
... mock_date.today.return_value = date(2010, 10, 8) | ... mock_date.today.return_value = date(2010, 10, 8) | ||
... mock_date.side_effect = lambda *args, **kw: date(*args, **kw) | ... mock_date.side_effect = lambda *args, **kw: date(*args, **kw) | ||
... | ... | ||
... assert mymodule.date.today() == date(2010, 10, 8) | ... assert mymodule.date.today() == date(2010, 10, 8) | ||
− | ... assert mymodule.date(2009, 6, 8) == date(2009, 6, 8)</ | + | ... assert mymodule.date(2009, 6, 8) == date(2009, 6, 8)</syntaxhighlight> |
</div> | </div> | ||
</div> | </div> | ||
− | + | 请注意,我们不会全局修补 [[../datetime#datetime|datetime.date]],而是在 ''使用'' 的模块中修补 <code>date</code>。 见[[../unittest|在哪里打补丁]]。 | |
− | |||
− | + | 当 <code>date.today()</code> 被调用时,会返回一个已知日期,但对 <code>date(...)</code> 构造函数的调用仍然返回正常日期。 如果没有这个,你会发现自己必须使用与被测代码完全相同的算法来计算预期结果,这是一种经典的测试反模式。 | |
− | <code>date(...)</code> | ||
− | |||
− | |||
− | + | 对日期构造函数的调用记录在 <code>mock_date</code> 属性(<code>call_count</code> 和朋友)中,这也可能对您的测试有用。 | |
− | |||
− | + | 在 [https://williambert.online/2011/07/how-to-unit-testing-in-django-with-mocking-and-patching/ 这个博客条目] 中讨论了另一种处理模拟日期或其他内置类的方法。 | |
− | |||
第886行: | 第765行: | ||
<div id="mocking-a-generator-method" class="section"> | <div id="mocking-a-generator-method" class="section"> | ||
− | === | + | === 模拟生成器方法 === |
− | + | Python 生成器是一个函数或方法,它使用 [[../../reference/simple_stmts#yield|yield]] 语句在迭代 [[#id3|1]] 时返回一系列值。 | |
− | |||
− | + | 调用生成器方法/函数以返回生成器对象。 然后迭代的是生成器对象。 迭代的协议方法是 [[../stdtypes#container|__iter__()]],所以我们可以使用 [[../unittest.mock#unittest.mock|MagicMock]] 来模拟它。 | |
− | |||
− | |||
− | |||
− | + | 这是一个示例类,其“iter”方法实现为生成器: | |
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第902行: | 第777行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> class Foo: |
... def iter(self): | ... def iter(self): | ||
... for i in [1, 2, 3]: | ... for i in [1, 2, 3]: | ||
... yield i | ... yield i | ||
... | ... | ||
− | + | >>> foo = Foo() | |
− | + | >>> list(foo.iter()) | |
− | [1, 2, 3]</ | + | [1, 2, 3]</syntaxhighlight> |
</div> | </div> | ||
</div> | </div> | ||
− | + | 我们将如何模拟这个类,尤其是它的“iter”方法? | |
− | + | 要配置迭代返回的值(隐含在调用 [[../stdtypes#list|list]] 中),我们需要配置调用返回的对象 <code>foo.iter()</code>。 | |
− | [[../stdtypes#list| | ||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第923行: | 第797行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> mock_foo = MagicMock() |
− | + | >>> mock_foo.iter.return_value = iter([1, 2, 3]) | |
− | + | >>> list(mock_foo.iter()) | |
− | [1, 2, 3]</ | + | [1, 2, 3]</syntaxhighlight> |
</div> | </div> | ||
第932行: | 第806行: | ||
</div> | </div> | ||
; <span class="brackets">[[#id2|1]]</span> | ; <span class="brackets">[[#id2|1]]</span> | ||
− | : | + | : 还有生成器表达式和更多 [http://www.dabeaz.com/coroutines/index.html 生成器的高级用法] ,但我们在这里不关心它们。 对生成器及其强大功能的非常好的介绍:[http://www.dabeaz.com/generators/ 面向系统程序员的生成器技巧]。 |
第938行: | 第812行: | ||
<div id="applying-the-same-patch-to-every-test-method" class="section"> | <div id="applying-the-same-patch-to-every-test-method" class="section"> | ||
− | === | + | === 对每个测试方法应用相同的补丁 === |
− | + | 如果您想为多个测试方法设置多个补丁,显而易见的方法是将补丁装饰器应用于每个方法。 这感觉像是不必要的重复。 对于 Python 2.6 或更新版本,您可以使用 [[../unittest.mock#unittest.mock|patch()]](以各种形式)作为类装饰器。 这将补丁应用于类上的所有测试方法。 测试方法由名称以 <code>test</code> 开头的方法标识: | |
− | |||
− | |||
− | |||
− | |||
− | |||
<div class="highlight-python3 notranslate"> | <div class="highlight-python3 notranslate"> | ||
第951行: | 第820行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python3">>>> @patch('mymodule.SomeClass') |
... class MyTest(unittest.TestCase): | ... class MyTest(unittest.TestCase): | ||
... | ... | ||
第963行: | 第832行: | ||
... return 'something' | ... return 'something' | ||
... | ... | ||
− | + | >>> MyTest('test_one').test_one() | |
− | + | >>> MyTest('test_two').test_two() | |
− | + | >>> MyTest('test_two').not_a_test() | |
− | 'something'</ | + | 'something'</syntaxhighlight> |
</div> | </div> | ||
</div> | </div> | ||
− | + | 管理补丁的另一种方法是使用 [[../unittest|补丁方法:start 和 stop]]。 这些允许您将补丁移动到 <code>setUp</code> 和 <code>tearDown</code> 方法中。 | |
− | |||
<div class="highlight-python3 notranslate"> | <div class="highlight-python3 notranslate"> | ||
第978行: | 第846行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python3">>>> class MyTest(unittest.TestCase): |
... def setUp(self): | ... def setUp(self): | ||
... self.patcher = patch('mymodule.foo') | ... self.patcher = patch('mymodule.foo') | ||
第989行: | 第857行: | ||
... self.patcher.stop() | ... self.patcher.stop() | ||
... | ... | ||
− | + | >>> MyTest('test_foo').run()</syntaxhighlight> | |
</div> | </div> | ||
</div> | </div> | ||
− | + | 如果您使用这种技术,您必须通过调用 <code>stop</code> 来确保补丁被“撤销”。 这可能比您想象的要复杂,因为如果在 setUp 中引发异常,则不会调用 tearDown。 [[../unittest#unittest.TestCase|unittest.TestCase.addCleanup()]] 使这更容易: | |
− | |||
− | |||
− | [[../unittest#unittest.TestCase| | ||
<div class="highlight-python3 notranslate"> | <div class="highlight-python3 notranslate"> | ||
第1,003行: | 第868行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python3">>>> class MyTest(unittest.TestCase): |
... def setUp(self): | ... def setUp(self): | ||
... patcher = patch('mymodule.foo') | ... patcher = patch('mymodule.foo') | ||
第1,012行: | 第877行: | ||
... self.assertIs(mymodule.foo, self.mock_foo) | ... self.assertIs(mymodule.foo, self.mock_foo) | ||
... | ... | ||
− | + | >>> MyTest('test_foo').run()</syntaxhighlight> | |
</div> | </div> | ||
第1,021行: | 第886行: | ||
<div id="mocking-unbound-methods" class="section"> | <div id="mocking-unbound-methods" class="section"> | ||
− | === | + | === 模拟未绑定的方法 === |
− | + | 在今天编写测试时,我需要修补一个 ''未绑定方法'' (修补类上的方法而不是实例上的方法)。 我需要将 self 作为第一个参数传入,因为我想断言哪些对象正在调用此特定方法。 问题是您无法为此使用模拟进行修补,因为如果您用模拟替换未绑定的方法,则从实例中获取时它不会成为绑定方法,因此不会自我传入。 解决方法是用一个真正的函数来修补未绑定的方法。 [[../unittest.mock#unittest.mock|patch()]] 装饰器使得用模拟修补方法变得如此简单,以至于必须创建一个真正的函数变得很麻烦。 | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | 如果您将 <code>autospec=True</code> 传递给 patch,那么它会使用 ''real'' 函数对象进行修补。 这个函数对象与它正在替换的函数对象具有相同的签名,但在幕后委托给一个模拟。 您仍然以与以前完全相同的方式自动创建模拟。 但这意味着,如果您使用它来修补类上的未绑定方法,则如果从实例中获取模拟函数,则该模拟函数将转换为绑定方法。 它将 <code>self</code> 作为第一个参数传入,这正是我想要的: | |
− | ''real'' | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第1,047行: | 第896行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> class Foo: |
... def foo(self): | ... def foo(self): | ||
... pass | ... pass | ||
... | ... | ||
− | + | >>> with patch.object(Foo, 'foo', autospec=True) as mock_foo: | |
... mock_foo.return_value = 'foo' | ... mock_foo.return_value = 'foo' | ||
... foo = Foo() | ... foo = Foo() | ||
第1,057行: | 第906行: | ||
... | ... | ||
'foo' | 'foo' | ||
− | + | >>> mock_foo.assert_called_once_with(foo)</syntaxhighlight> | |
</div> | </div> | ||
</div> | </div> | ||
− | + | 如果我们不使用 <code>autospec=True</code>,那么未绑定的方法会用 Mock 实例修补,而不是用 <code>self</code> 调用。 | |
− | |||
第1,069行: | 第917行: | ||
<div id="checking-multiple-calls-with-mock" class="section"> | <div id="checking-multiple-calls-with-mock" class="section"> | ||
− | === | + | === 使用模拟检查多个调用 === |
− | mock | + | mock 有一个很好的 API 来断言你的模拟对象是如何使用的。 |
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第1,077行: | 第925行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> mock = Mock() |
− | + | >>> mock.foo_bar.return_value = None | |
− | + | >>> mock.foo_bar('baz', spam='eggs') | |
− | + | >>> mock.foo_bar.assert_called_with('baz', spam='eggs')</syntaxhighlight> | |
</div> | </div> | ||
</div> | </div> | ||
− | + | 如果您的模拟只被调用一次,您可以使用 <code>assert_called_once_with()</code> 方法,该方法也断言 <code>call_count</code> 是一个。 | |
− | <code>assert_called_once_with()</code> | ||
− | <code>call_count</code> | ||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第1,093行: | 第939行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> mock.foo_bar.assert_called_once_with('baz', spam='eggs') |
− | + | >>> mock.foo_bar() | |
− | + | >>> mock.foo_bar.assert_called_once_with('baz', spam='eggs') | |
Traceback (most recent call last): | Traceback (most recent call last): | ||
... | ... | ||
− | AssertionError: Expected to be called once. Called 2 times.</ | + | AssertionError: Expected to be called once. Called 2 times.</syntaxhighlight> |
</div> | </div> | ||
</div> | </div> | ||
− | + | <code>assert_called_with</code> 和 <code>assert_called_once_with</code> 都对 ''最近的'' 调用做出断言。 如果您的模拟将被多次调用,并且您想对 ''all'' 这些调用进行断言,您可以使用 [[../unittest.mock#unittest.mock.Mock|call_args_list]]: | |
− | |||
− | |||
− | [[../unittest.mock#unittest.mock.Mock| | ||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第1,112行: | 第955行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> mock = Mock(return_value=None) |
− | + | >>> mock(1, 2, 3) | |
− | + | >>> mock(4, 5, 6) | |
− | + | >>> mock() | |
− | + | >>> mock.call_args_list | |
− | [call(1, 2, 3), call(4, 5, 6), call()]</ | + | [call(1, 2, 3), call(4, 5, 6), call()]</syntaxhighlight> |
</div> | </div> | ||
</div> | </div> | ||
− | + | [[../unittest.mock#unittest.mock|call]] 助手可以轻松地对这些调用进行断言。 您可以建立一个预期调用列表并将其与 <code>call_args_list</code> 进行比较。 这看起来与 <code>call_args_list</code> 的再现非常相似: | |
− | |||
− | |||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第1,130行: | 第971行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> expected = [call(1, 2, 3), call(4, 5, 6), call()] |
− | + | >>> mock.call_args_list == expected | |
− | True</ | + | True</syntaxhighlight> |
</div> | </div> | ||
第1,141行: | 第982行: | ||
<div id="coping-with-mutable-arguments" class="section"> | <div id="coping-with-mutable-arguments" class="section"> | ||
− | === | + | === 处理可变参数 === |
− | + | 另一种情况很少见,但会咬你,就是当你的模拟被可变参数调用时。 <code>call_args</code> 和 <code>call_args_list</code> 存储对参数的 ''引用'' 。 如果参数被测试中的代码改变了,那么你不能再断言调用模拟时的值是什么。 | |
− | |||
− | |||
− | |||
− | + | 这是一些显示问题的示例代码。 想象一下在“mymodule”中定义的以下函数: | |
− | |||
<div class="highlight-python3 notranslate"> | <div class="highlight-python3 notranslate"> | ||
第1,155行: | 第992行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python3">def frob(val): |
pass | pass | ||
def grob(val): | def grob(val): | ||
− | + | "First frob and then clear val" | |
frob(val) | frob(val) | ||
− | val.clear()</ | + | val.clear()</syntaxhighlight> |
</div> | </div> | ||
</div> | </div> | ||
− | + | 当我们尝试测试 <code>grob</code> 使用正确的参数调用 <code>frob</code> 时,看看会发生什么: | |
− | |||
<div class="highlight-python3 notranslate"> | <div class="highlight-python3 notranslate"> | ||
第1,173行: | 第1,009行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python3">>>> with patch('mymodule.frob') as mock_frob: |
... val = {6} | ... val = {6} | ||
... mymodule.grob(val) | ... mymodule.grob(val) | ||
... | ... | ||
− | + | >>> val | |
set() | set() | ||
− | + | >>> mock_frob.assert_called_with({6}) | |
Traceback (most recent call last): | Traceback (most recent call last): | ||
... | ... | ||
AssertionError: Expected: (({6},), {}) | AssertionError: Expected: (({6},), {}) | ||
− | Called with: ((set(),), {})</ | + | Called with: ((set(),), {})</syntaxhighlight> |
</div> | </div> | ||
</div> | </div> | ||
− | + | 一种可能性是模拟复制您传入的参数。 如果你做的断言依赖于对象身份的相等性,那么这可能会导致问题。 | |
− | |||
− | |||
− | + | 这是使用 <code>side_effect</code> 功能的一种解决方案。 如果您为模拟提供 <code>side_effect</code> 函数,则将使用与模拟相同的参数调用 <code>side_effect</code>。 这使我们有机会复制参数并将它们存储以供以后的断言使用。 在这个例子中,我使用 ''another'' 模拟来存储参数,以便我可以使用模拟方法进行断言。 一个辅助函数再次为我设置了这个。 | |
− | |||
− | <code>side_effect</code> | ||
− | |||
− | |||
− | |||
− | |||
<div class="highlight-python3 notranslate"> | <div class="highlight-python3 notranslate"> | ||
第1,204行: | 第1,032行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python3">>>> from copy import deepcopy |
− | + | >>> from unittest.mock import Mock, patch, DEFAULT | |
− | + | >>> def copy_call_args(mock): | |
... new_mock = Mock() | ... new_mock = Mock() | ||
... def side_effect(*args, **kwargs): | ... def side_effect(*args, **kwargs): | ||
第1,216行: | 第1,044行: | ||
... return new_mock | ... return new_mock | ||
... | ... | ||
− | + | >>> with patch('mymodule.frob') as mock_frob: | |
... new_mock = copy_call_args(mock_frob) | ... new_mock = copy_call_args(mock_frob) | ||
... val = {6} | ... val = {6} | ||
... mymodule.grob(val) | ... mymodule.grob(val) | ||
... | ... | ||
− | + | >>> new_mock.assert_called_with({6}) | |
− | + | >>> new_mock.call_args | |
− | call({6})</ | + | call({6})</syntaxhighlight> |
</div> | </div> | ||
</div> | </div> | ||
− | <code>copy_call_args</code> | + | <code>copy_call_args</code> 与将被调用的模拟一起调用。 它返回一个我们对其进行断言的新模拟。 <code>side_effect</code> 函数制作了 args 的副本,并使用副本调用我们的 <code>new_mock</code>。 |
− | |||
− | |||
<div class="admonition note"> | <div class="admonition note"> | ||
− | + | 笔记 | |
− | + | 如果您的模拟只使用一次,则有一种更简单的方法可以在调用时检查参数。 您可以简单地在 <code>side_effect</code> 函数内部进行检查。 | |
− | |||
− | |||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第1,244行: | 第1,068行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> def side_effect(arg): |
... assert arg == {6} | ... assert arg == {6} | ||
... | ... | ||
− | + | >>> mock = Mock(side_effect=side_effect) | |
− | + | >>> mock({6}) | |
− | + | >>> mock(set()) | |
Traceback (most recent call last): | Traceback (most recent call last): | ||
... | ... | ||
− | AssertionError</ | + | AssertionError</syntaxhighlight> |
</div> | </div> | ||
第1,259行: | 第1,083行: | ||
</div> | </div> | ||
− | + | 另一种方法是创建一个 [[../unittest.mock#unittest.mock|Mock]] 或 [[../unittest.mock#unittest.mock|MagicMock]] 的子类来复制(使用 [[../copy#copy|copy.deepcopy()]])参数。 这是一个示例实现: | |
− | [[../unittest.mock#unittest.mock| | ||
− | |||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第1,267行: | 第1,089行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> from copy import deepcopy |
− | + | >>> class CopyingMock(MagicMock): | |
... def __call__(self, /, *args, **kwargs): | ... def __call__(self, /, *args, **kwargs): | ||
... args = deepcopy(args) | ... args = deepcopy(args) | ||
... kwargs = deepcopy(kwargs) | ... kwargs = deepcopy(kwargs) | ||
− | ... return super( | + | ... return super().__call__(*args, **kwargs) |
... | ... | ||
− | + | >>> c = CopyingMock(return_value=None) | |
− | + | >>> arg = set() | |
− | + | >>> c(arg) | |
− | + | >>> arg.add(1) | |
− | + | >>> c.assert_called_with(set()) | |
− | + | >>> c.assert_called_with(arg) | |
Traceback (most recent call last): | Traceback (most recent call last): | ||
... | ... | ||
AssertionError: Expected call: mock({1}) | AssertionError: Expected call: mock({1}) | ||
Actual call: mock(set()) | Actual call: mock(set()) | ||
− | + | >>> c.foo | |
− | + | <CopyingMock name='mock.foo' id='...'></syntaxhighlight> | |
</div> | </div> | ||
</div> | </div> | ||
− | + | 当您对 <code>Mock</code> 或 <code>MagicMock</code> 所有动态创建的属性进行子类化时,<code>return_value</code> 将自动使用您的子类。 这意味着 <code>CopyingMock</code> 的所有子代也将具有 <code>CopyingMock</code> 类型。 | |
− | |||
− | |||
第1,298行: | 第1,118行: | ||
<div id="nesting-patches" class="section"> | <div id="nesting-patches" class="section"> | ||
− | === | + | === 嵌套补丁 === |
− | + | 使用补丁作为上下文管理器很好,但如果你做多个补丁,你最终可能会嵌套使用越来越向右缩进的语句: | |
− | |||
− | |||
<div class="highlight-python3 notranslate"> | <div class="highlight-python3 notranslate"> | ||
第1,308行: | 第1,126行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python3">>>> class MyTest(unittest.TestCase): |
... | ... | ||
... def test_foo(self): | ... def test_foo(self): | ||
第1,318行: | 第1,136行: | ||
... assert mymodule.Spam is mock_spam | ... assert mymodule.Spam is mock_spam | ||
... | ... | ||
− | + | >>> original = mymodule.Foo | |
− | + | >>> MyTest('test_foo').test_foo() | |
− | + | >>> assert mymodule.Foo is original</syntaxhighlight> | |
</div> | </div> | ||
</div> | </div> | ||
− | + | 使用 unittest <code>cleanup</code> 函数和 [[../unittest|补丁方法:start 和 stop]] 我们可以在没有嵌套缩进的情况下实现相同的效果。 一个简单的辅助方法 <code>create_patch</code> 将补丁放置到位并为我们返回创建的模拟: | |
− | |||
− | |||
− | |||
<div class="highlight-python3 notranslate"> | <div class="highlight-python3 notranslate"> | ||
第1,334行: | 第1,149行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python3">>>> class MyTest(unittest.TestCase): |
... | ... | ||
... def create_patch(self, name): | ... def create_patch(self, name): | ||
第1,351行: | 第1,166行: | ||
... assert mymodule.Spam is mock_spam | ... assert mymodule.Spam is mock_spam | ||
... | ... | ||
− | + | >>> original = mymodule.Foo | |
− | + | >>> MyTest('test_foo').run() | |
− | + | >>> assert mymodule.Foo is original</syntaxhighlight> | |
</div> | </div> | ||
第1,362行: | 第1,177行: | ||
<div id="mocking-a-dictionary-with-magicmock" class="section"> | <div id="mocking-a-dictionary-with-magicmock" class="section"> | ||
− | === | + | === 用 MagicMock 模拟字典 === |
− | + | 你可能想要模拟一个字典或其他容器对象,记录对它的所有访问,同时让它仍然像字典一样运行。 | |
− | |||
− | + | 我们可以使用 [[../unittest.mock#unittest.mock|MagicMock]] 来做到这一点,它的行为类似于字典,并使用 [[../unittest.mock#unittest.mock.Mock|side_effect]] 将字典访问委托给我们控制的真实底层字典。 | |
− | |||
− | |||
− | + | 当我们的 <code>MagicMock</code> 的 <code>__getitem__()</code> 和 <code>__setitem__()</code> 方法被调用(正常字典访问)时,<code>side_effect</code> 将使用键调用(在 [ X154X] 值太)。 我们还可以控制返回的内容。 | |
− | ( | ||
− | |||
− | + | 使用 <code>MagicMock</code> 后,我们可以使用 [[../unittest.mock#unittest.mock.Mock|call_args_list]] 等属性来断言字典的使用方式: | |
− | [[../unittest.mock#unittest.mock.Mock| | ||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第1,382行: | 第1,191行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> my_dict = {'a': 1, 'b': 2, 'c': 3} |
− | + | >>> def getitem(name): | |
... return my_dict[name] | ... return my_dict[name] | ||
... | ... | ||
− | + | >>> def setitem(name, val): | |
... my_dict[name] = val | ... my_dict[name] = val | ||
... | ... | ||
− | + | >>> mock = MagicMock() | |
− | + | >>> mock.__getitem__.side_effect = getitem | |
− | + | >>> mock.__setitem__.side_effect = setitem</syntaxhighlight> | |
</div> | </div> | ||
第1,398行: | 第1,207行: | ||
<div class="admonition note"> | <div class="admonition note"> | ||
− | + | 笔记 | |
− | + | 使用 <code>MagicMock</code> 的替代方法是使用 <code>Mock</code> 和 ''only'' 提供您特别想要的魔术方法: | |
− | |||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第1,407行: | 第1,215行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> mock = Mock() |
− | + | >>> mock.__getitem__ = Mock(side_effect=getitem) | |
− | + | >>> mock.__setitem__ = Mock(side_effect=setitem)</syntaxhighlight> | |
</div> | </div> | ||
</div> | </div> | ||
− | + | ''third'' 选项是使用 <code>MagicMock</code> 但传入 <code>dict</code> 作为 ''spec''(或 ''spec_set'')参数,以便<code>MagicMock</code> 创建的只有字典魔术方法可用: | |
− | |||
− | |||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第1,422行: | 第1,228行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> mock = MagicMock(spec_set=dict) |
− | + | >>> mock.__getitem__.side_effect = getitem | |
− | + | >>> mock.__setitem__.side_effect = setitem</syntaxhighlight> | |
</div> | </div> | ||
第1,431行: | 第1,237行: | ||
</div> | </div> | ||
− | + | 有了这些副作用功能,<code>mock</code> 将像普通字典一样运行,但会记录访问。 如果您尝试访问不存在的密钥,它甚至会引发 [[../exceptions#KeyError|KeyError]]。 | |
− | |||
− | |||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第1,439行: | 第1,243行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> mock['a'] |
1 | 1 | ||
− | + | >>> mock['c'] | |
3 | 3 | ||
− | + | >>> mock['d'] | |
Traceback (most recent call last): | Traceback (most recent call last): | ||
... | ... | ||
KeyError: 'd' | KeyError: 'd' | ||
− | + | >>> mock['b'] = 'fish' | |
− | + | >>> mock['d'] = 'eggs' | |
− | + | >>> mock['b'] | |
'fish' | 'fish' | ||
− | + | >>> mock['d'] | |
− | 'eggs'</ | + | 'eggs'</syntaxhighlight> |
</div> | </div> | ||
</div> | </div> | ||
− | + | 使用后,您可以使用正常的模拟方法和属性对访问进行断言: | |
− | |||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第1,464行: | 第1,267行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> mock.__getitem__.call_args_list |
[call('a'), call('c'), call('d'), call('b'), call('d')] | [call('a'), call('c'), call('d'), call('b'), call('d')] | ||
− | + | >>> mock.__setitem__.call_args_list | |
[call('b', 'fish'), call('d', 'eggs')] | [call('b', 'fish'), call('d', 'eggs')] | ||
− | + | >>> my_dict | |
− | {'a': 1, 'b': 'fish', 'c': 3, 'd': 'eggs'}</ | + | {'a': 1, 'b': 'fish', 'c': 3, 'd': 'eggs'}</syntaxhighlight> |
</div> | </div> | ||
第1,478行: | 第1,281行: | ||
<div id="mock-subclasses-and-their-attributes" class="section"> | <div id="mock-subclasses-and-their-attributes" class="section"> | ||
− | === | + | === 模拟子类及其属性 === |
− | + | 您可能想要子类化 [[../unittest.mock#unittest.mock|Mock]] 的原因有很多。 原因之一可能是添加辅助方法。 这是一个愚蠢的例子: | |
− | |||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第1,487行: | 第1,289行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> class MyMock(MagicMock): |
... def has_been_called(self): | ... def has_been_called(self): | ||
... return self.called | ... return self.called | ||
... | ... | ||
− | + | >>> mymock = MyMock(return_value=None) | |
− | + | >>> mymock | |
− | + | <MyMock id='...'> | |
− | + | >>> mymock.has_been_called() | |
False | False | ||
− | + | >>> mymock() | |
− | + | >>> mymock.has_been_called() | |
− | True</ | + | True</syntaxhighlight> |
</div> | </div> | ||
</div> | </div> | ||
− | + | <code>Mock</code> 实例的标准行为是属性和返回值模拟与访问它们的模拟具有相同的类型。 这确保 <code>Mock</code> 属性为 <code>Mocks</code>,<code>MagicMock</code> 属性为 <code>MagicMocks</code> [[#id5|2]]。 因此,如果您要子类化以添加辅助方法,那么它们也将可用于您的子类实例的属性和返回值模拟。 | |
− | |||
− | |||
− | [[#id5|2]] | ||
− | |||
− | |||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第1,514行: | 第1,311行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> mymock.foo |
− | + | <MyMock name='mock.foo' id='...'> | |
− | + | >>> mymock.foo.has_been_called() | |
False | False | ||
− | + | >>> mymock.foo() | |
− | + | <MyMock name='mock.foo()' id='...'> | |
− | + | >>> mymock.foo.has_been_called() | |
− | True</ | + | True</syntaxhighlight> |
</div> | </div> | ||
</div> | </div> | ||
− | + | 有时这很不方便。 例如, [https://code.google.com/archive/p/mock/issues/105 一个用户] 正在子类化模拟以创建一个 [https://twistedmatrix.com/documents/11.0.0/api/twisted.python.components.html 扭曲适配器] 。 将此应用于属性实际上会导致错误。 | |
− | |||
− | |||
− | <code>Mock</code> | + | <code>Mock</code>(在其所有风格中)使用称为 <code>_get_child_mock</code> 的方法为属性和返回值创建这些“子模拟”。 您可以通过覆盖此方法来防止您的子类用于属性。 签名是它接受任意关键字参数(<code>**kwargs</code>),然后将其传递给模拟构造函数: |
− | |||
− | |||
− | |||
− | |||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第1,540行: | 第1,331行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> class Subclass(MagicMock): |
... def _get_child_mock(self, /, **kwargs): | ... def _get_child_mock(self, /, **kwargs): | ||
... return MagicMock(**kwargs) | ... return MagicMock(**kwargs) | ||
... | ... | ||
− | + | >>> mymock = Subclass() | |
− | + | >>> mymock.foo | |
− | + | <MagicMock name='mock.foo' id='...'> | |
− | + | >>> assert isinstance(mymock, Subclass) | |
− | + | >>> assert not isinstance(mymock.foo, Subclass) | |
− | + | >>> assert not isinstance(mymock(), Subclass)</syntaxhighlight> | |
</div> | </div> | ||
第1,555行: | 第1,346行: | ||
</div> | </div> | ||
; <span class="brackets">[[#id4|2]]</span> | ; <span class="brackets">[[#id4|2]]</span> | ||
− | : | + | : 此规则的一个例外是不可调用的模拟。 属性使用可调用变体,因为否则不可调用的模拟不能具有可调用的方法。 |
第1,561行: | 第1,352行: | ||
<div id="mocking-imports-with-patch-dict" class="section"> | <div id="mocking-imports-with-patch-dict" class="section"> | ||
− | === | + | === 使用 patch.dict 模拟导入 === |
− | + | 模拟可能很难的一种情况是您在函数中具有本地导入。 这些更难模拟,因为它们没有使用我们可以修补的模块命名空间中的对象。 | |
− | |||
− | |||
− | + | 通常应避免本地进口。 有时这样做是为了防止循环依赖,为此 ''通常'' 有更好的方法来解决问题(重构代码)或通过延迟导入来防止“前期成本”。 这也可以通过比无条件本地导入更好的方式来解决(将模块存储为类或模块属性,并且仅在第一次使用时进行导入)。 | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | 除此之外,还有一种方法可以使用 <code>mock</code> 来影响导入的结果。 导入从 [[../sys#sys|sys.modules]] 字典中获取一个 ''object''。 请注意,它获取了一个 ''object'',它不需要是一个模块。 第一次导入模块会导致模块对象被放入 sys.modules,所以通常当你导入某些东西时,你会得到一个模块。 然而,情况不必如此。 | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | 这意味着您可以使用 [[../unittest.mock#unittest.mock.patch|patch.dict()]] 到 ''临时'' 在 [[../sys#sys|sys.modules]] 中放置一个模拟。 此补丁处于活动状态时的任何导入都将获取模拟。 当补丁完成时(装饰函数退出,with 语句体完成或 <code>patcher.stop()</code> 被调用),那么之前的任何内容都将安全恢复。 | |
− | |||
− | |||
− | |||
− | |||
− | + | 这是一个模拟“fooble”模块的示例。 | |
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第1,593行: | 第1,368行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> import sys |
− | + | >>> mock = Mock() | |
− | + | >>> with patch.dict('sys.modules', {'fooble': mock}): | |
... import fooble | ... import fooble | ||
... fooble.blob() | ... fooble.blob() | ||
... | ... | ||
− | + | <Mock name='mock.blob()' id='...'> | |
− | + | >>> assert 'fooble' not in sys.modules | |
− | + | >>> mock.blob.assert_called_once_with()</syntaxhighlight> | |
</div> | </div> | ||
</div> | </div> | ||
− | + | 如您所见,<code>import fooble</code> 成功,但退出时 [[../sys#sys|sys.modules]] 中没有“傻瓜”。 | |
− | |||
− | + | 这也适用于 <code>from module import name</code> 形式: | |
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第1,615行: | 第1,389行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> mock = Mock() |
− | + | >>> with patch.dict('sys.modules', {'fooble': mock}): | |
... from fooble import blob | ... from fooble import blob | ||
... blob.blip() | ... blob.blip() | ||
... | ... | ||
− | + | <Mock name='mock.blob.blip()' id='...'> | |
− | + | >>> mock.blob.blip.assert_called_once_with()</syntaxhighlight> | |
</div> | </div> | ||
</div> | </div> | ||
− | + | 稍微多做一点工作,您还可以模拟包导入: | |
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第1,632行: | 第1,406行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> mock = Mock() |
− | + | >>> modules = {'package': mock, 'package.module': mock.module} | |
− | + | >>> with patch.dict('sys.modules', modules): | |
... from package.module import fooble | ... from package.module import fooble | ||
... fooble() | ... fooble() | ||
... | ... | ||
− | + | <Mock name='mock.module.fooble()' id='...'> | |
− | + | >>> mock.module.fooble.assert_called_once_with()</syntaxhighlight> | |
</div> | </div> | ||
第1,648行: | 第1,422行: | ||
<div id="tracking-order-of-calls-and-less-verbose-call-assertions" class="section"> | <div id="tracking-order-of-calls-and-less-verbose-call-assertions" class="section"> | ||
− | === | + | === 跟踪调用顺序和不那么冗长的调用断言 === |
− | + | [[../unittest.mock#unittest.mock|Mock]] 类允许您通过 [[../unittest.mock#unittest.mock.Mock|method_calls]] 属性跟踪模拟对象上方法调用的 ''order''。 这不允许您跟踪单独模拟对象之间的调用顺序,但是我们可以使用 [[../unittest.mock#unittest.mock.Mock|mock_calls]] 来实现相同的效果。 | |
− | |||
− | |||
− | |||
− | + | 因为模拟跟踪对 <code>mock_calls</code> 中子模拟的调用,并且访问模拟的任意属性会创建一个子模拟,所以我们可以从父模拟创建单独的模拟。 对这些子模拟的调用将按顺序记录在父级的 <code>mock_calls</code> 中: | |
− | |||
− | |||
− | |||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第1,664行: | 第1,432行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> manager = Mock() |
− | + | >>> mock_foo = manager.foo | |
− | + | >>> mock_bar = manager.bar</syntaxhighlight> | |
</div> | </div> | ||
第1,675行: | 第1,443行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> mock_foo.something() |
− | + | <Mock name='mock.foo.something()' id='...'> | |
− | + | >>> mock_bar.other.thing() | |
− | + | <Mock name='mock.bar.other.thing()' id='...'></syntaxhighlight> | |
</div> | </div> | ||
第1,687行: | 第1,455行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> manager.mock_calls |
− | [call.foo.something(), call.bar.other.thing()]</ | + | [call.foo.something(), call.bar.other.thing()]</syntaxhighlight> |
</div> | </div> | ||
</div> | </div> | ||
− | + | 然后,我们可以通过与管理器模拟上的 <code>mock_calls</code> 属性进行比较来断言调用,包括顺序: | |
− | |||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第1,700行: | 第1,467行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> expected_calls = [call.foo.something(), call.bar.other.thing()] |
− | + | >>> manager.mock_calls == expected_calls | |
− | True</ | + | True</syntaxhighlight> |
</div> | </div> | ||
</div> | </div> | ||
− | + | 如果 <code>patch</code> 正在创建并放置您的模拟,那么您可以使用 [[../unittest.mock#unittest.mock.Mock|attach_mock()]] 方法将它们附加到管理器模拟。 挂接电话后会记录在管理员的<code>mock_calls</code>中。 | |
− | |||
− | |||
<div class="highlight-python3 notranslate"> | <div class="highlight-python3 notranslate"> | ||
第1,715行: | 第1,480行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python3">>>> manager = MagicMock() |
− | + | >>> with patch('mymodule.Class1') as MockClass1: | |
... with patch('mymodule.Class2') as MockClass2: | ... with patch('mymodule.Class2') as MockClass2: | ||
... manager.attach_mock(MockClass1, 'MockClass1') | ... manager.attach_mock(MockClass1, 'MockClass1') | ||
第1,722行: | 第1,487行: | ||
... MockClass1().foo() | ... MockClass1().foo() | ||
... MockClass2().bar() | ... MockClass2().bar() | ||
− | + | <MagicMock name='mock.MockClass1().foo()' id='...'> | |
− | + | <MagicMock name='mock.MockClass2().bar()' id='...'> | |
− | + | >>> manager.mock_calls | |
[call.MockClass1(), | [call.MockClass1(), | ||
call.MockClass1().foo(), | call.MockClass1().foo(), | ||
call.MockClass2(), | call.MockClass2(), | ||
− | call.MockClass2().bar()]</ | + | call.MockClass2().bar()]</syntaxhighlight> |
</div> | </div> | ||
</div> | </div> | ||
− | + | 如果进行了多次调用,但您只对它们的特定序列感兴趣,那么另一种方法是使用 [[../unittest.mock#unittest.mock.Mock|assert_has_calls()]] 方法。 这需要一个调用列表(用 [[../unittest.mock#unittest.mock|call]] 对象构造)。 如果该调用序列在 [[../unittest.mock#unittest.mock.Mock|mock_calls]] 中,则断言成功。 | |
− | |||
− | [[../unittest.mock#unittest.mock.Mock| | ||
− | |||
− | [[../unittest.mock#unittest.mock.Mock| | ||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第1,743行: | 第1,504行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> m = MagicMock() |
− | + | >>> m().foo().bar().baz() | |
− | + | <MagicMock name='mock().foo().bar().baz()' id='...'> | |
− | + | >>> m.one().two().three() | |
− | + | <MagicMock name='mock.one().two().three()' id='...'> | |
− | + | >>> calls = call.one().two().three().call_list() | |
− | + | >>> m.assert_has_calls(calls)</syntaxhighlight> | |
</div> | </div> | ||
</div> | </div> | ||
− | + | 即使链接调用 <code>m.one().two().three()</code> 不是对模拟进行的唯一调用,断言仍然成功。 | |
− | |||
− | + | 有时,模拟可能会对其进行多次调用,而您只对断言这些调用中的 ''some'' 感兴趣。 您甚至可能不关心订单。 在这种情况下,您可以将 <code>any_order=True</code> 传递给 <code>assert_has_calls</code>: | |
− | |||
− | |||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第1,765行: | 第1,523行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> m = MagicMock() |
− | + | >>> m(1), m.two(2, 3), m.seven(7), m.fifty('50') | |
(...) | (...) | ||
− | + | >>> calls = [call.fifty('50'), call(1), call.seven(7)] | |
− | + | >>> m.assert_has_calls(calls, any_order=True)</syntaxhighlight> | |
</div> | </div> | ||
第1,778行: | 第1,536行: | ||
<div id="more-complex-argument-matching" class="section"> | <div id="more-complex-argument-matching" class="section"> | ||
− | === | + | === 更复杂的参数匹配 === |
− | + | 使用与 [[../unittest.mock#unittest.mock|ANY]] 相同的基本概念,我们可以实现匹配器对用作模拟参数的对象进行更复杂的断言。 | |
− | |||
− | + | 假设我们希望将某个对象传递给一个模拟,该模拟默认情况下根据对象标识进行比较(这是用户定义类的 Python 默认值)。 要使用 [[../unittest.mock#unittest.mock.Mock|assert_called_with()]] 我们需要传入完全相同的对象。 如果我们只对这个对象的某些属性感兴趣,那么我们可以创建一个匹配器来为我们检查这些属性。 | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | 您可以在此示例中看到对 <code>assert_called_with</code> 的“标准”调用是不够的: | |
− | |||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第1,797行: | 第1,548行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> class Foo: |
... def __init__(self, a, b): | ... def __init__(self, a, b): | ||
... self.a, self.b = a, b | ... self.a, self.b = a, b | ||
... | ... | ||
− | + | >>> mock = Mock(return_value=None) | |
− | + | >>> mock(Foo(1, 2)) | |
− | + | >>> mock.assert_called_with(Foo(1, 2)) | |
Traceback (most recent call last): | Traceback (most recent call last): | ||
... | ... | ||
− | AssertionError: Expected: call( | + | AssertionError: Expected: call(<__main__.Foo object at 0x...>) |
− | Actual call: call( | + | Actual call: call(<__main__.Foo object at 0x...>)</syntaxhighlight> |
</div> | </div> | ||
</div> | </div> | ||
− | + | 我们的 <code>Foo</code> 类的比较函数可能如下所示: | |
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第1,818行: | 第1,569行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> def compare(self, other): |
... if not type(self) == type(other): | ... if not type(self) == type(other): | ||
... return False | ... return False | ||
第1,826行: | 第1,577行: | ||
... return False | ... return False | ||
... return True | ... return True | ||
− | ...</ | + | ...</syntaxhighlight> |
</div> | </div> | ||
</div> | </div> | ||
− | + | 一个可以使用这样的比较函数进行相等运算的匹配器对象看起来像这样: | |
− | |||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第1,838行: | 第1,588行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> class Matcher: |
... def __init__(self, compare, some_obj): | ... def __init__(self, compare, some_obj): | ||
... self.compare = compare | ... self.compare = compare | ||
第1,844行: | 第1,594行: | ||
... def __eq__(self, other): | ... def __eq__(self, other): | ||
... return self.compare(self.some_obj, other) | ... return self.compare(self.some_obj, other) | ||
− | ...</ | + | ...</syntaxhighlight> |
</div> | </div> | ||
</div> | </div> | ||
− | + | 将所有这些放在一起: | |
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第1,855行: | 第1,605行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> match_foo = Matcher(compare, Foo(1, 2)) |
− | + | >>> mock.assert_called_with(match_foo)</syntaxhighlight> | |
</div> | </div> | ||
</div> | </div> | ||
− | + | <code>Matcher</code> 用我们的比较函数和我们想要比较的 <code>Foo</code> 对象实例化。 在 <code>assert_called_with</code> 中,将调用 <code>Matcher</code> 相等方法,它将调用模拟的对象与我们创建匹配器的对象进行比较。 如果它们匹配,则 <code>assert_called_with</code> 通过,如果它们不匹配,则引发 [[../exceptions#AssertionError|AssertionError]]: | |
− | |||
− | |||
− | |||
− | <code>assert_called_with</code> | ||
<div class="doctest highlight-default notranslate"> | <div class="doctest highlight-default notranslate"> | ||
第1,871行: | 第1,617行: | ||
<div class="highlight"> | <div class="highlight"> | ||
− | < | + | <syntaxhighlight lang="python">>>> match_wrong = Matcher(compare, Foo(3, 4)) |
− | + | >>> mock.assert_called_with(match_wrong) | |
Traceback (most recent call last): | Traceback (most recent call last): | ||
... | ... | ||
− | AssertionError: Expected: (( | + | AssertionError: Expected: ((<Matcher object at 0x...>,), {}) |
− | Called with: (( | + | Called with: ((<Foo object at 0x...>,), {})</syntaxhighlight> |
</div> | </div> | ||
</div> | </div> | ||
− | + | 通过一些调整,您可以让比较函数直接引发 [[../exceptions#AssertionError|AssertionError]] 并提供更有用的失败消息。 | |
− | [[../exceptions#AssertionError| | ||
− | + | 从 1.5 版开始,Python 测试库 [https://pyhamcrest.readthedocs.io/ PyHamcrest] 以相等匹配器 ([https://pyhamcrest.readthedocs.io/en/release-1.8/integration/#module-hamcrest.library.integration.match_equality hamcrest.library.integration.match_equality]) 的形式提供了类似的功能,在这里可能很有用。 | |
− | |||
− | ([https://pyhamcrest.readthedocs.io/en/release-1.8/integration/#module-hamcrest.library.integration.match_equality hamcrest.library.integration.match_equality]) | ||
第1,894行: | 第1,637行: | ||
</div> | </div> | ||
+ | <div class="clearer"> | ||
− | [[Category:Python 3.9 | + | |
+ | |||
+ | </div> | ||
+ | |||
+ | [[Category:Python 3.9 文档]] |
2021年10月31日 (日) 04:54的最新版本
unittest.mock — 入门
3.3 版中的新功能。
使用模拟
模拟修补方法
Mock 对象的常见用途包括:
- 修补方法
- 记录对象的方法调用
您可能想要替换对象上的方法以检查系统的另一部分是否使用正确的参数调用它:
一旦使用了我们的模拟(在本例中为 real.method
),它就会有一些方法和属性,可以让你断言它是如何被使用的。
一旦模拟被调用,它的 called 属性被设置为 True
。 更重要的是,我们可以使用 assert_called_with() 或 assert_Called_once_with() 方法来检查它是否使用正确的参数被调用。
此示例测试调用 ProductionClass().method
会导致调用 something
方法:
模拟对象上的方法调用
在上一个示例中,我们直接在对象上修补了一个方法以检查它是否被正确调用。 另一个常见的用例是将一个对象传递给一个方法(或被测系统的某个部分),然后检查它是否以正确的方式使用。
下面简单的ProductionClass
有一个closer
方法。 如果它被一个对象调用,那么它会在它上面调用 close
。
所以为了测试它,我们需要使用 close
方法传入一个对象并检查它是否被正确调用。
我们不需要做任何工作来在我们的模拟上提供 'close' 方法。 访问 close 创建它。 因此,如果 'close' 尚未被调用,则在测试中访问它会创建它,但 assert_Called_with() 将引发失败异常。
模拟类
一个常见的用例是模拟由您的被测代码实例化的类。 当您修补一个类时,该类将被替换为模拟。 实例是通过 调用类 创建的。 这意味着您可以通过查看模拟类的返回值来访问“模拟实例”。
在下面的例子中,我们有一个函数 some_function
实例化 Foo
并调用它的方法。 对 patch() 的调用用模拟替换了类 Foo
。 Foo
实例是调用mock的结果,所以通过修改mockreturn_value来配置。
命名你的模拟
给你的模拟一个名字会很有用。 该名称显示在模拟的 repr 中,当模拟出现在测试失败消息中时会很有帮助。 该名称还传播到模拟的属性或方法:
跟踪所有呼叫
通常,您希望跟踪对方法的多次调用。 mock_calls 属性记录了对模拟的子属性的所有调用——以及对它们的子属性的调用。
如果您对 mock_calls
进行断言并且调用了任何意外方法,则该断言将失败。 这很有用,因为除了断言您期望的调用已经进行之外,您还可以检查它们是否以正确的顺序进行并且没有额外的调用:
您使用 call 对象来构造列表以与 mock_calls
进行比较:
但是,不会记录返回模拟的调用的参数,这意味着无法跟踪用于创建祖先的参数很重要的嵌套调用:
设置返回值和属性
在模拟对象上设置返回值非常简单:
当然,您可以对模拟上的方法执行相同的操作:
返回值也可以在构造函数中设置:
如果您需要在模拟上设置属性,请执行以下操作:
有时您想模拟更复杂的情况,例如 mock.connection.cursor().execute("SELECT 1")
。 如果我们希望这个调用返回一个列表,那么我们必须配置嵌套调用的结果。
我们可以使用 call 在像这样的“链式调用”中构造调用集,以便之后轻松断言:
正是对 .call_list()
的调用将我们的调用对象转换为代表链接调用的调用列表。
副作用函数和迭代
side_effect
也可以设置为一个函数或一个可迭代对象。 side_effect
作为可迭代对象的用例是您的模拟将被多次调用,并且您希望每次调用都返回不同的值。 当您将 side_effect
设置为可迭代对象时,每次调用模拟都会返回可迭代对象的下一个值:
对于更高级的用例,例如根据调用模拟的内容动态改变返回值,side_effect
可以是一个函数。 将使用与模拟相同的参数调用该函数。 无论函数返回什么,调用都会返回什么:
模拟异步迭代器
从 Python 3.8 开始,AsyncMock
和 MagicMock
支持通过 __aiter__
模拟 Asynchronous Iterators。 __aiter__
的 return_value 属性可用于设置要用于迭代的返回值。
模拟异步上下文管理器
从 Python 3.8 开始,AsyncMock
和 MagicMock
支持通过 __aenter__
和 __aexit__
模拟 异步上下文管理器。 默认情况下,__aenter__
和 __aexit__
是返回异步函数的 AsyncMock
实例。
从现有对象创建模拟
过度使用模拟的一个问题是它将您的测试与模拟的实现相结合,而不是您的真实代码。 假设您有一个实现 some_method
的类。 在另一个类的测试中,您提供了 也 提供 some_method
的此对象的模拟。 如果稍后您重构第一个类,使其不再具有 some_method
- 那么即使您的代码现在已损坏,您的测试仍将继续通过!
Mock 允许您提供一个对象作为模拟的规范,使用 spec 关键字参数。 访问您的规范对象上不存在的模拟上的方法/属性将立即引发属性错误。 如果您更改规范的实现,则使用该类的测试将立即开始失败,而无需在这些测试中实例化该类。
使用规范还可以更智能地匹配对模拟的调用,无论某些参数是作为位置参数还是命名参数传递的:
如果您希望这种更智能的匹配也适用于模拟上的方法调用,您可以使用 auto-speccing。
如果您想要一种更强大的规范形式来防止设置任意属性以及获取它们,那么您可以使用 spec_set 而不是 spec。
补丁装饰器
测试中的一个常见需求是修补类属性或模块属性,例如修补内置模块或修补模块中的类以测试它是否已实例化。 模块和类实际上是全局的,因此必须在测试后取消对它们的修补,否则修补程序将持续存在于其他测试中并导致难以诊断的问题。
mock 为此提供了三个方便的装饰器:patch()、patch.object() 和 patch.dict()。 patch
采用单个字符串,形式为 package.module.Class.attribute
来指定您正在修补的属性。 它还可选地采用您希望替换属性(或类或其他)的值。 'patch.object' 接受一个对象和你想要修补的属性的名称,加上可选的值来修补它。
patch.object
:
如果您正在修补模块(包括 builtins),则使用 patch() 而不是 patch.object():
如果需要,模块名称可以“带点”,格式为 package.module
:
一个不错的模式是实际装饰测试方法本身:
如果你想用一个 Mock 打补丁,你可以使用只有一个参数的 patch() (或带有两个参数的 patch.object() )。 将为您创建模拟并传递给测试函数/方法:
您可以使用此模式堆叠多个补丁装饰器:
当您嵌套补丁装饰器时,模拟以它们应用的相同顺序传递给装饰函数(应用装饰器的正常 Python 顺序)。 这意味着自下而上,因此在上面的示例中,首先传入 test_module.ClassName2
的模拟。
还有 patch.dict() 用于在范围内设置字典中的值,并在测试结束时将字典恢复到其原始状态:
patch
、patch.object
和 patch.dict
都可以用作上下文管理器。
在您使用 patch() 为您创建模拟的地方,您可以使用 with 语句的“as”形式获取对模拟的引用:
作为替代 patch
,patch.object
和 patch.dict
可以用作类装饰器。 以这种方式使用时,它与将装饰器单独应用于名称以“test”开头的每个方法相同。
进一步的例子
下面是一些稍微更高级的场景的更多示例。
模拟链式调用
一旦你理解了 return_value 属性,模拟链调用实际上很简单。 当第一次调用模拟时,或者在调用之前获取它的 return_value
时,会创建一个新的 Mock。
这意味着您可以通过询问 return_value
模拟来了解从调用模拟对象返回的对象是如何被使用的:
从这里开始,这是一个简单的配置步骤,然后对链式调用进行断言。 当然,另一种选择是首先以更可测试的方式编写代码……
所以,假设我们有一些看起来像这样的代码:
假设BackendProvider
已经测试好了,我们如何测试method()
? 具体来说,我们要测试代码段 # more code
是否以正确的方式使用响应对象。
由于此调用链是从实例属性进行的,我们可以在 Something
实例上对 backend
属性进行猴子修补。 在这种特殊情况下,我们只对最终调用 start_call
的返回值感兴趣,因此我们没有太多配置要做。 让我们假设它返回的对象是“类文件”,因此我们将确保我们的响应对象使用内置的 open() 作为其 spec
。
为此,我们创建一个模拟实例作为我们的模拟后端,并为其创建一个模拟响应对象。 要将响应设置为最终 start_call
的返回值,我们可以这样做:
我们可以使用 configure_mock() 方法以更好的方式直接为我们设置返回值:
有了这些,我们就可以修补“模拟后端”,并可以进行真正的调用:
使用 mock_calls 我们可以使用单个断言检查链接的调用。 一个链式调用是一行代码中的多个调用,所以在mock_calls
中会有多个条目。 我们可以使用 call.call_list() 为我们创建这个调用列表:
部分嘲讽
在某些测试中,我想模拟对 datetime.date.today() 的调用以返回已知日期,但我不想阻止被测代码创建新的日期对象。 不幸的是 datetime.date 是用 C 编写的,所以我不能只是修补静态 date.today()
方法。
我找到了一种简单的方法来做到这一点,该方法涉及用模拟有效地包装日期类,但将构造函数的调用传递给真正的类(并返回真实的实例)。
patch 装饰器 用于模拟被测模块中的 date
类。 然后将模拟日期类的 side_effect
属性设置为返回真实日期的 lambda 函数。 当模拟日期类被调用时,side_effect
将构造并返回一个真实日期。
请注意,我们不会全局修补 datetime.date,而是在 使用 的模块中修补 date
。 见在哪里打补丁。
当 date.today()
被调用时,会返回一个已知日期,但对 date(...)
构造函数的调用仍然返回正常日期。 如果没有这个,你会发现自己必须使用与被测代码完全相同的算法来计算预期结果,这是一种经典的测试反模式。
对日期构造函数的调用记录在 mock_date
属性(call_count
和朋友)中,这也可能对您的测试有用。
在 这个博客条目 中讨论了另一种处理模拟日期或其他内置类的方法。
模拟生成器方法
Python 生成器是一个函数或方法,它使用 yield 语句在迭代 1 时返回一系列值。
调用生成器方法/函数以返回生成器对象。 然后迭代的是生成器对象。 迭代的协议方法是 __iter__(),所以我们可以使用 MagicMock 来模拟它。
这是一个示例类,其“iter”方法实现为生成器:
我们将如何模拟这个类,尤其是它的“iter”方法?
要配置迭代返回的值(隐含在调用 list 中),我们需要配置调用返回的对象 foo.iter()
。
- 1
- 还有生成器表达式和更多 生成器的高级用法 ,但我们在这里不关心它们。 对生成器及其强大功能的非常好的介绍:面向系统程序员的生成器技巧。
对每个测试方法应用相同的补丁
如果您想为多个测试方法设置多个补丁,显而易见的方法是将补丁装饰器应用于每个方法。 这感觉像是不必要的重复。 对于 Python 2.6 或更新版本,您可以使用 patch()(以各种形式)作为类装饰器。 这将补丁应用于类上的所有测试方法。 测试方法由名称以 test
开头的方法标识:
管理补丁的另一种方法是使用 补丁方法:start 和 stop。 这些允许您将补丁移动到 setUp
和 tearDown
方法中。
如果您使用这种技术,您必须通过调用 stop
来确保补丁被“撤销”。 这可能比您想象的要复杂,因为如果在 setUp 中引发异常,则不会调用 tearDown。 unittest.TestCase.addCleanup() 使这更容易:
模拟未绑定的方法
在今天编写测试时,我需要修补一个 未绑定方法 (修补类上的方法而不是实例上的方法)。 我需要将 self 作为第一个参数传入,因为我想断言哪些对象正在调用此特定方法。 问题是您无法为此使用模拟进行修补,因为如果您用模拟替换未绑定的方法,则从实例中获取时它不会成为绑定方法,因此不会自我传入。 解决方法是用一个真正的函数来修补未绑定的方法。 patch() 装饰器使得用模拟修补方法变得如此简单,以至于必须创建一个真正的函数变得很麻烦。
如果您将 autospec=True
传递给 patch,那么它会使用 real 函数对象进行修补。 这个函数对象与它正在替换的函数对象具有相同的签名,但在幕后委托给一个模拟。 您仍然以与以前完全相同的方式自动创建模拟。 但这意味着,如果您使用它来修补类上的未绑定方法,则如果从实例中获取模拟函数,则该模拟函数将转换为绑定方法。 它将 self
作为第一个参数传入,这正是我想要的:
如果我们不使用 autospec=True
,那么未绑定的方法会用 Mock 实例修补,而不是用 self
调用。
使用模拟检查多个调用
mock 有一个很好的 API 来断言你的模拟对象是如何使用的。
如果您的模拟只被调用一次,您可以使用 assert_called_once_with()
方法,该方法也断言 call_count
是一个。
assert_called_with
和 assert_called_once_with
都对 最近的 调用做出断言。 如果您的模拟将被多次调用,并且您想对 all 这些调用进行断言,您可以使用 call_args_list:
call 助手可以轻松地对这些调用进行断言。 您可以建立一个预期调用列表并将其与 call_args_list
进行比较。 这看起来与 call_args_list
的再现非常相似:
处理可变参数
另一种情况很少见,但会咬你,就是当你的模拟被可变参数调用时。 call_args
和 call_args_list
存储对参数的 引用 。 如果参数被测试中的代码改变了,那么你不能再断言调用模拟时的值是什么。
这是一些显示问题的示例代码。 想象一下在“mymodule”中定义的以下函数:
当我们尝试测试 grob
使用正确的参数调用 frob
时,看看会发生什么:
一种可能性是模拟复制您传入的参数。 如果你做的断言依赖于对象身份的相等性,那么这可能会导致问题。
这是使用 side_effect
功能的一种解决方案。 如果您为模拟提供 side_effect
函数,则将使用与模拟相同的参数调用 side_effect
。 这使我们有机会复制参数并将它们存储以供以后的断言使用。 在这个例子中,我使用 another 模拟来存储参数,以便我可以使用模拟方法进行断言。 一个辅助函数再次为我设置了这个。
copy_call_args
与将被调用的模拟一起调用。 它返回一个我们对其进行断言的新模拟。 side_effect
函数制作了 args 的副本,并使用副本调用我们的 new_mock
。
笔记
如果您的模拟只使用一次,则有一种更简单的方法可以在调用时检查参数。 您可以简单地在 side_effect
函数内部进行检查。
另一种方法是创建一个 Mock 或 MagicMock 的子类来复制(使用 copy.deepcopy())参数。 这是一个示例实现:
当您对 Mock
或 MagicMock
所有动态创建的属性进行子类化时,return_value
将自动使用您的子类。 这意味着 CopyingMock
的所有子代也将具有 CopyingMock
类型。
嵌套补丁
使用补丁作为上下文管理器很好,但如果你做多个补丁,你最终可能会嵌套使用越来越向右缩进的语句:
使用 unittest cleanup
函数和 补丁方法:start 和 stop 我们可以在没有嵌套缩进的情况下实现相同的效果。 一个简单的辅助方法 create_patch
将补丁放置到位并为我们返回创建的模拟:
用 MagicMock 模拟字典
你可能想要模拟一个字典或其他容器对象,记录对它的所有访问,同时让它仍然像字典一样运行。
我们可以使用 MagicMock 来做到这一点,它的行为类似于字典,并使用 side_effect 将字典访问委托给我们控制的真实底层字典。
当我们的 MagicMock
的 __getitem__()
和 __setitem__()
方法被调用(正常字典访问)时,side_effect
将使用键调用(在 [ X154X] 值太)。 我们还可以控制返回的内容。
使用 MagicMock
后,我们可以使用 call_args_list 等属性来断言字典的使用方式:
笔记
使用 MagicMock
的替代方法是使用 Mock
和 only 提供您特别想要的魔术方法:
third 选项是使用 MagicMock
但传入 dict
作为 spec(或 spec_set)参数,以便MagicMock
创建的只有字典魔术方法可用:
有了这些副作用功能,mock
将像普通字典一样运行,但会记录访问。 如果您尝试访问不存在的密钥,它甚至会引发 KeyError。
使用后,您可以使用正常的模拟方法和属性对访问进行断言:
模拟子类及其属性
您可能想要子类化 Mock 的原因有很多。 原因之一可能是添加辅助方法。 这是一个愚蠢的例子:
Mock
实例的标准行为是属性和返回值模拟与访问它们的模拟具有相同的类型。 这确保 Mock
属性为 Mocks
,MagicMock
属性为 MagicMocks
2。 因此,如果您要子类化以添加辅助方法,那么它们也将可用于您的子类实例的属性和返回值模拟。
有时这很不方便。 例如, 一个用户 正在子类化模拟以创建一个 扭曲适配器 。 将此应用于属性实际上会导致错误。
Mock
(在其所有风格中)使用称为 _get_child_mock
的方法为属性和返回值创建这些“子模拟”。 您可以通过覆盖此方法来防止您的子类用于属性。 签名是它接受任意关键字参数(**kwargs
),然后将其传递给模拟构造函数:
- 2
- 此规则的一个例外是不可调用的模拟。 属性使用可调用变体,因为否则不可调用的模拟不能具有可调用的方法。
使用 patch.dict 模拟导入
模拟可能很难的一种情况是您在函数中具有本地导入。 这些更难模拟,因为它们没有使用我们可以修补的模块命名空间中的对象。
通常应避免本地进口。 有时这样做是为了防止循环依赖,为此 通常 有更好的方法来解决问题(重构代码)或通过延迟导入来防止“前期成本”。 这也可以通过比无条件本地导入更好的方式来解决(将模块存储为类或模块属性,并且仅在第一次使用时进行导入)。
除此之外,还有一种方法可以使用 mock
来影响导入的结果。 导入从 sys.modules 字典中获取一个 object。 请注意,它获取了一个 object,它不需要是一个模块。 第一次导入模块会导致模块对象被放入 sys.modules,所以通常当你导入某些东西时,你会得到一个模块。 然而,情况不必如此。
这意味着您可以使用 patch.dict() 到 临时 在 sys.modules 中放置一个模拟。 此补丁处于活动状态时的任何导入都将获取模拟。 当补丁完成时(装饰函数退出,with 语句体完成或 patcher.stop()
被调用),那么之前的任何内容都将安全恢复。
这是一个模拟“fooble”模块的示例。
如您所见,import fooble
成功,但退出时 sys.modules 中没有“傻瓜”。
这也适用于 from module import name
形式:
稍微多做一点工作,您还可以模拟包导入:
跟踪调用顺序和不那么冗长的调用断言
Mock 类允许您通过 method_calls 属性跟踪模拟对象上方法调用的 order。 这不允许您跟踪单独模拟对象之间的调用顺序,但是我们可以使用 mock_calls 来实现相同的效果。
因为模拟跟踪对 mock_calls
中子模拟的调用,并且访问模拟的任意属性会创建一个子模拟,所以我们可以从父模拟创建单独的模拟。 对这些子模拟的调用将按顺序记录在父级的 mock_calls
中:
然后,我们可以通过与管理器模拟上的 mock_calls
属性进行比较来断言调用,包括顺序:
如果 patch
正在创建并放置您的模拟,那么您可以使用 attach_mock() 方法将它们附加到管理器模拟。 挂接电话后会记录在管理员的mock_calls
中。
如果进行了多次调用,但您只对它们的特定序列感兴趣,那么另一种方法是使用 assert_has_calls() 方法。 这需要一个调用列表(用 call 对象构造)。 如果该调用序列在 mock_calls 中,则断言成功。
即使链接调用 m.one().two().three()
不是对模拟进行的唯一调用,断言仍然成功。
有时,模拟可能会对其进行多次调用,而您只对断言这些调用中的 some 感兴趣。 您甚至可能不关心订单。 在这种情况下,您可以将 any_order=True
传递给 assert_has_calls
:
更复杂的参数匹配
使用与 ANY 相同的基本概念,我们可以实现匹配器对用作模拟参数的对象进行更复杂的断言。
假设我们希望将某个对象传递给一个模拟,该模拟默认情况下根据对象标识进行比较(这是用户定义类的 Python 默认值)。 要使用 assert_called_with() 我们需要传入完全相同的对象。 如果我们只对这个对象的某些属性感兴趣,那么我们可以创建一个匹配器来为我们检查这些属性。
您可以在此示例中看到对 assert_called_with
的“标准”调用是不够的:
我们的 Foo
类的比较函数可能如下所示:
一个可以使用这样的比较函数进行相等运算的匹配器对象看起来像这样:
将所有这些放在一起:
Matcher
用我们的比较函数和我们想要比较的 Foo
对象实例化。 在 assert_called_with
中,将调用 Matcher
相等方法,它将调用模拟的对象与我们创建匹配器的对象进行比较。 如果它们匹配,则 assert_called_with
通过,如果它们不匹配,则引发 AssertionError:
通过一些调整,您可以让比较函数直接引发 AssertionError 并提供更有用的失败消息。
从 1.5 版开始,Python 测试库 PyHamcrest 以相等匹配器 (hamcrest.library.integration.match_equality) 的形式提供了类似的功能,在这里可能很有用。