unittest.mock — 入门

在 3.3 版本中添加。

使用 Mock

Mock 补丁方法

常见用法 Mock 对象包括

  • 补丁方法

  • 记录对象上的方法调用

你可能希望替换对象上的方法,以检查系统中的另一部分是否使用正确参数调用了该方法

>>> real = SomeClass()
>>> real.method = MagicMock(name='method')
>>> real.method(3, 4, 5, key='value')
<MagicMock name='method()' id='...'>

一旦我们的模拟被使用(在本例中为 real.method),它就有方法和属性,允许你对它的使用方式做出断言。

注意

在大多数这些示例中,MockMagicMock 类是可互换的。由于 MagicMock 是功能更强大的类,因此默认情况下使用它很明智。

一旦模拟被调用,它的 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)

模拟对象上的方法调用

在最后一个示例中,我们直接在对象上修补了一个方法,以检查它是否被正确调用。另一个常见的用例是将对象传递到方法(或被测系统的一部分)中,然后检查它是否以正确的方式使用。

下面的简单 ProductionClass 有一个 closer 方法。如果它被一个对象调用,那么它就会调用该对象的 close

>>> class ProductionClass:
...     def closer(self, something):
...         something.close()
...

因此,为了测试它,我们需要传入一个带有 close 方法的对象,并检查它是否被正确调用。

>>> real = ProductionClass()
>>> mock = Mock()
>>> real.closer(mock)
>>> mock.close.assert_called_with()

我们不必做任何工作来为我们的模拟提供“close”方法。访问 close 会创建它。因此,如果尚未调用“close”,那么在测试中访问它将创建它,但 assert_called_with() 将引发失败异常。

模拟类

一个常见的用例是模拟被测代码实例化的类。当您修补一个类时,该类将被一个模拟替换。实例是通过调用类创建的。这意味着您可以通过查看模拟类的返回值来访问“模拟实例”。

在下面的示例中,我们有一个函数 some_function,它实例化 Foo 并调用其上的一个方法。对 patch() 的调用用一个模拟替换类 FooFoo 实例是调用模拟的结果,因此它可以通过修改模拟 return_value 进行配置。

>>> 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'

为您的模拟命名

为您的模拟命名可能很有用。名称显示在模拟的 repr 中,当模拟出现在测试失败消息中时,这可能很有帮助。该名称还会传播到模拟的属性或方法

>>> mock = MagicMock(name='foo')
>>> mock
<MagicMock name='foo' id='...'>
>>> mock.method
<MagicMock name='foo.method' id='...'>

跟踪所有调用

通常,您希望跟踪对方法的多个调用。 mock_calls 属性记录对模拟的子属性的所有调用,还记录对它们的子属性的调用。

>>> 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)]

如果您对 mock_calls 进行断言,并且调用了任何意外的方法,那么断言将失败。这很有用,因为除了断言您期望的调用已被执行之外,您还检查它们是否按正确的顺序执行,并且没有额外的调用

您可以使用 call 对象构造与 mock_calls 进行比较的列表

>>> expected = [call.method(), call.attribute.method(10, x=53)]
>>> mock.mock_calls == expected
True

但是,不会记录返回模拟的调用的参数,这意味着无法跟踪参数用于创建祖先的重要嵌套调用

>>> m = Mock()
>>> m.factory(important=True).deliver()
<Mock name='mock.factory().deliver()' id='...'>
>>> m.mock_calls[-1] == call.factory(important=False).deliver()
True

设置返回值和属性

在模拟对象上设置返回值非常容易

>>> mock = Mock()
>>> mock.return_value = 3
>>> mock()
3

当然,您也可以对模拟中的方法执行相同操作

>>> mock = Mock()
>>> mock.method.return_value = 3
>>> mock.method()
3

也可以在构造函数中设置返回值

>>> mock = Mock(return_value=3)
>>> mock()
3

如果您需要在模拟中设置属性,请直接执行

>>> mock = Mock()
>>> mock.x = 3
>>> mock.x
3

有时您希望模拟更复杂的情况,例如 mock.connection.cursor().execute("SELECT 1")。如果我们希望此调用返回列表,则必须配置嵌套调用的结果。

我们可以使用 call 为“链式调用”构建一组调用,以便于之后进行断言

>>> 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
[call.connection.cursor(), call.connection.cursor().execute('SELECT 1')]
>>> mock.mock_calls == expected
True

正是对 .call_list() 的调用将我们的调用对象转换为表示链式调用的调用列表。

使用模拟引发异常

一个有用的属性是 side_effect。如果您将其设置为异常类或实例,则在调用模拟时将引发异常。

>>> mock = Mock(side_effect=Exception('Boom!'))
>>> mock()
Traceback (most recent call last):
  ...
Exception: Boom!

副作用函数和可迭代对象

side_effect 还可以设置为函数或可迭代对象。将 side_effect 作为可迭代对象使用的情况是您的模拟将被调用多次,并且您希望每次调用都返回不同的值。当您将 side_effect 设置为可迭代对象时,对模拟的每次调用都会从可迭代对象返回下一个值

>>> mock = MagicMock(side_effect=[4, 5, 6])
>>> mock()
4
>>> mock()
5
>>> mock()
6

对于更高级的用例,例如根据模拟调用的内容动态更改返回值,side_effect 可以是函数。该函数将使用与模拟相同的参数调用。函数返回的任何内容都是调用返回的内容

>>> vals = {(1, 2): 1, (2, 3): 2}
>>> def side_effect(*args):
...     return vals[args]
...
>>> mock = MagicMock(side_effect=side_effect)
>>> mock(1, 2)
1
>>> mock(2, 3)
2

模拟异步迭代器

自 Python 3.8 起,AsyncMockMagicMock 支持通过 __aiter__ 模拟 异步迭代器__aiter__return_value 属性可用于设置要用于迭代的返回值。

>>> mock = MagicMock()  # AsyncMock also works here
>>> mock.__aiter__.return_value = [1, 2, 3]
>>> async def main():
...     return [i async for i in mock]
...
>>> asyncio.run(main())
[1, 2, 3]

模拟异步上下文管理器

自 Python 3.8 起,AsyncMockMagicMock 支持通过 __aenter____aexit__ 模拟 异步上下文管理器。默认情况下,__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 also works here
>>> 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()

从现有对象创建模拟

过度使用模拟的一个问题是,它将你的测试与模拟的实现而不是你的真实代码耦合在一起。假设你有一个实现 some_method 的类。在对另一个类的测试中,你提供了此对象的模拟,它提供了 some_method。如果你稍后重构第一个类,使其不再具有 some_method,那么你的测试将继续通过,即使你的代码现在已损坏!

Mock 允许你提供一个对象作为模拟的规范,使用 spec 关键字参数。访问模拟中不存在于你的规范对象上的方法/属性将立即引发属性错误。如果你更改了规范的实现,那么使用该类的测试将立即开始失败,而无需在这些测试中实例化该类。

>>> mock = Mock(spec=SomeClass)
>>> mock.old_method()
Traceback (most recent call last):
   ...
AttributeError: object has no attribute 'old_method'

使用规范还可以更智能地匹配对模拟的调用,无论某些参数是作为位置参数还是命名参数传递的

>>> 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)

如果你希望这种更智能的匹配也适用于模拟上的方法调用,则可以使用 自动规范

如果你想要一种更强的规范形式,它可以防止设置任意属性以及获取它们,那么你可以使用 spec_set 而不是 spec

使用 side_effect 返回每个文件的内容

mock_open() 用于修补 open() 方法。 side_effect 可用于在每次调用时返回一个新的模拟对象。这可用于返回存储在字典中的每个文件中的不同内容

DEFAULT = "default"
data_dict = {"file1": "data1",
             "file2": "data2"}

def open_side_effect(name):
    return mock_open(read_data=data_dict.get(name, DEFAULT))()

with patch("builtins.open", side_effect=open_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() == "default"

修补装饰器

注意

对于 patch(),重要的是在查找它们时修补命名空间中的对象。这通常很简单,但有关快速指南,请阅读 修补位置

测试中的一个常见需求是修补类属性或模块属性,例如修补内置函数或修补模块中的类以测试是否对其进行了实例化。模块和类实际上是全局的,因此必须在测试后撤消对它们的修补,否则修补将持续到其他测试中并导致难以诊断的问题。

mock 为此提供了三个方便的装饰器:patch()patch.object()patch.dict()patch 采用一个字符串,格式为 package.module.Class.attribute,以指定要修补的属性。它还可以选择采用一个值,用该值替换属性(或类或其他任何内容)。“patch.object”采用一个对象和要修补的属性的名称,以及可选地采用要修补它的值。

patch.object:

>>> original = SomeClass.attribute
>>> @patch.object(SomeClass, 'attribute', sentinel.attribute)
... def test():
...     assert SomeClass.attribute == sentinel.attribute
...
>>> test()
>>> assert SomeClass.attribute == original

>>> @patch('package.module.attribute', sentinel.attribute)
... def test():
...     from package.module import attribute
...     assert attribute is sentinel.attribute
...
>>> test()

如果你要修补一个模块(包括 builtins),请使用 patch() 代替 patch.object()

>>> mock = MagicMock(return_value=sentinel.file_handle)
>>> with patch('builtins.open', mock):
...     handle = open('filename', 'r')
...
>>> mock.assert_called_with('filename', 'r')
>>> assert handle == sentinel.file_handle, "incorrect file handle returned"

如果需要,模块名称可以是“点分”的,格式为 package.module

>>> @patch('package.module.ClassName.attribute', sentinel.attribute)
... def test():
...     from package.module import ClassName
...     assert ClassName.attribute == sentinel.attribute
...
>>> test()

一种不错的模式是实际装饰测试方法本身

>>> class MyTest(unittest.TestCase):
...     @patch.object(SomeClass, 'attribute', sentinel.attribute)
...     def test_something(self):
...         self.assertEqual(SomeClass.attribute, sentinel.attribute)
...
>>> original = SomeClass.attribute
>>> MyTest('test_something').test_something()
>>> assert SomeClass.attribute == original

如果你想用 Mock 修补,你可以只使用一个参数(或 patch.object() 使用两个参数)来使用 patch()。将为你创建 Mock 并将其传递到测试函数/方法

>>> class MyTest(unittest.TestCase):
...     @patch.object(SomeClass, 'static_method')
...     def test_something(self, mock_method):
...         SomeClass.static_method()
...         mock_method.assert_called_with()
...
>>> MyTest('test_something').test_something()

你可以使用此模式堆叠多个修补装饰器

>>> 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)
...
>>> MyTest('test_something').test_something()

当你嵌套修补装饰器时,Mock 会按应用它们的顺序传递到已装饰的函数中(应用装饰器的正常Python 顺序)。这意味着从下到上,因此在上面的示例中,首先传递 test_module.ClassName2 的 Mock。

还有 patch.dict(),用于在某个范围内设置字典中的值,并在测试结束时将字典还原到其原始状态

>>> foo = {'key': 'value'}
>>> original = foo.copy()
>>> with patch.dict(foo, {'newkey': 'newvalue'}, clear=True):
...     assert foo == {'newkey': 'newvalue'}
...
>>> assert foo == original

patchpatch.objectpatch.dict 都可以用作上下文管理器。

在使用 patch() 为你创建模拟时,你可以使用 with 语句的“as”形式获取模拟的引用

>>> class ProductionClass:
...     def method(self):
...         pass
...
>>> with patch.object(ProductionClass, 'method') as mock_method:
...     mock_method.return_value = None
...     real = ProductionClass()
...     real.method(1, 2, 3)
...
>>> mock_method.assert_called_with(1, 2, 3)

作为替代,patchpatch.objectpatch.dict 可以用作类装饰器。当以这种方式使用时,它与将装饰器单独应用于名称以“test”开头的每个方法相同。

更多示例

以下是一些针对一些稍微高级一点的场景的更多示例。

模拟链式调用

一旦你理解了 return_value 属性,使用 mock 模拟链式调用实际上很简单。当首次调用模拟时,或者在你调用它之前获取其 return_value 时,将创建一个新的 Mock

这意味着你可以通过查询 return_value 模拟来查看从对模拟对象的调用返回的对象是如何使用的

>>> mock = Mock()
>>> mock().foo(a=2, b=3)
<Mock name='mock().foo()' id='...'>
>>> mock.return_value.foo.assert_called_with(a=2, b=3)

从这里开始,配置并断言链式调用非常简单。当然,另一种选择是首先以更可测试的方式编写代码…

因此,假设我们有一些看起来像这样的代码

>>> 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

假设 BackendProvider 已经过充分测试,我们如何测试 method()?具体来说,我们希望测试代码部分 # more code 是否以正确的方式使用响应对象。

由于此调用链是从实例属性进行的,因此我们可以对 Something 实例上的 backend 属性进行 monkey 补丁。在此特定情况下,我们只对 start_call 的最终调用的返回值感兴趣,因此我们不需要进行太多配置。让我们假设它返回的对象是“类文件”,因此我们将确保我们的响应对象使用内置 open() 作为其 spec

为此,我们创建一个模拟实例作为我们的模拟后端,并为其创建一个模拟响应对象。要将响应设置为该最终 start_call 的返回值,我们可以这样做

mock_backend.get_endpoint.return_value.create_call.return_value.start_call.return_value = mock_response

我们可以使用 configure_mock() 方法直接为我们设置返回值,从而以一种稍微更好的方式来完成此操作

>>> 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)

使用这些方法,我们可以在适当的位置修补“模拟后端”,并且可以进行实际调用

>>> something.backend = mock_backend
>>> something.method()

使用 mock_calls,我们可以使用单个断言来检查链式调用。链式调用是代码一行中的多个调用,因此 mock_calls 中将有多个条目。我们可以使用 call.call_list() 为我们创建此调用列表

>>> chained = call.get_endpoint('foobar').create_call('spam', 'eggs').start_call()
>>> call_list = chained.call_list()
>>> assert mock_backend.mock_calls == call_list

部分模拟

在某些测试中,我想模拟对 datetime.date.today() 的调用以返回一个已知日期,但我不想阻止被测代码创建新的日期对象。不幸的是,datetime.date 是用 C 编写的,因此我无法仅仅修补掉静态 datetime.date.today() 方法。

我找到了一种简单的方法来执行此操作,该方法有效地使用模拟包装日期类,但将对构造函数的调用传递给实际类(并返回实际实例)。

这里使用 patch decorator 来模拟被测模块中的 date 类。然后将模拟日期类上的 side_effect 属性设置为返回实际日期的 lambda 函数。当调用模拟日期类时,side_effect 将构造并返回实际日期。

>>> from datetime import date
>>> with patch('mymodule.date') as mock_date:
...     mock_date.today.return_value = date(2010, 10, 8)
...     mock_date.side_effect = lambda *args, **kw: date(*args, **kw)
...
...     assert mymodule.date.today() == date(2010, 10, 8)
...     assert mymodule.date(2009, 6, 8) == date(2009, 6, 8)

请注意,我们不会全局修补 datetime.date,我们修补使用它的模块中的 date。请参阅 修补位置

当调用 date.today() 时,会返回一个已知日期,但对 date(...) 构造函数的调用仍会返回正常日期。如果没有这一点,你可能会发现自己不得不使用与被测代码完全相同的算法来计算预期结果,这是一个经典的测试反模式。

对日期构造函数的调用会记录在 mock_date 属性(call_count 及其相关属性)中,这对于你的测试也可能很有用。

这篇博客文章讨论了处理模拟日期或其他内置类的另一种方法。

模拟生成器方法

Python 生成器是一个函数或方法,它使用 yield 语句在迭代时返回一系列值 [1]

调用生成器方法/函数以返回生成器对象。然后对生成器对象进行迭代。迭代的协议方法是 __iter__(),因此我们可以使用 MagicMock 对其进行模拟。

这是一个示例类,其中“iter”方法实现为生成器

>>> class Foo:
...     def iter(self):
...         for i in [1, 2, 3]:
...             yield i
...
>>> foo = Foo()
>>> list(foo.iter())
[1, 2, 3]

我们如何模拟这个类,特别是它的“iter”方法?

要配置从迭代中返回的值(隐含在对 list 的调用中),我们需要配置由对 foo.iter() 的调用返回的对象。

>>> mock_foo = MagicMock()
>>> mock_foo.iter.return_value = iter([1, 2, 3])
>>> list(mock_foo.iter())
[1, 2, 3]

对每个测试方法应用相同的修补程序

如果你希望对多个测试方法应用多个修补程序,那么显而易见的方法是对每个方法应用修补程序装饰器。这可能会感觉像是不必要的重复。相反,你可以将 patch()(以其各种形式)用作类装饰器。这会将修补程序应用到类上的所有测试方法。测试方法由名称以 test 开头的那些方法标识

>>> @patch('mymodule.SomeClass')
... class MyTest(unittest.TestCase):
...
...     def test_one(self, MockSomeClass):
...         self.assertIs(mymodule.SomeClass, MockSomeClass)
...
...     def test_two(self, MockSomeClass):
...         self.assertIs(mymodule.SomeClass, MockSomeClass)
...
...     def not_a_test(self):
...         return 'something'
...
>>> MyTest('test_one').test_one()
>>> MyTest('test_two').test_two()
>>> MyTest('test_two').not_a_test()
'something'

管理补丁的另一种方法是使用 patch 方法:start 和 stop。这些方法允许你将补丁移动到 setUptearDown 方法中。

>>> class MyTest(unittest.TestCase):
...     def setUp(self):
...         self.patcher = patch('mymodule.foo')
...         self.mock_foo = self.patcher.start()
...
...     def test_foo(self):
...         self.assertIs(mymodule.foo, self.mock_foo)
...
...     def tearDown(self):
...         self.patcher.stop()
...
>>> MyTest('test_foo').run()

如果你使用此技术,你必须确保通过调用 stop 来“撤销”补丁。这可能比你想象的更麻烦,因为如果在 setUp 中引发异常,则不会调用 tearDown。 unittest.TestCase.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)
...
>>> MyTest('test_foo').run()

模拟未绑定方法

今天在编写测试时,我需要修补一个未绑定方法(在类上修补方法,而不是在实例上)。我需要将 self 作为第一个参数传入,因为我想断言哪些对象调用了此特定方法。问题在于你无法为此修补一个模拟,因为如果你用模拟替换一个未绑定方法,那么从实例中获取它时它不会变成一个绑定方法,因此它不会传入 self。解决方法是用一个真实函数来修补未绑定方法。 patch() 装饰器使得用模拟来修补方法变得如此简单,以至于必须创建一个真实函数会成为一种麻烦。

如果你将 autospec=True 传递给 patch,那么它将用一个真实函数对象进行修补。此函数对象具有与它所替换的对象相同的签名,但在幕后委托给一个模拟。你仍然会以与之前完全相同的方式自动创建模拟。这意味着,如果你使用它来修补类上的未绑定方法,那么如果从实例中获取模拟函数,它将变成一个绑定方法。它将把 self 作为第一个参数传入,这正是我想要的

>>> class Foo:
...   def foo(self):
...     pass
...
>>> with patch.object(Foo, 'foo', autospec=True) as mock_foo:
...   mock_foo.return_value = 'foo'
...   foo = Foo()
...   foo.foo()
...
'foo'
>>> mock_foo.assert_called_once_with(foo)

如果我们不使用 autospec=True,那么未绑定方法将用一个 Mock 实例进行修补,并且不会用 self 进行调用。

使用 mock 检查多个调用

mock 有一个很好的 API,用于对模拟对象的用法进行断言。

>>> mock = Mock()
>>> mock.foo_bar.return_value = None
>>> mock.foo_bar('baz', spam='eggs')
>>> mock.foo_bar.assert_called_with('baz', spam='eggs')

如果你的模拟只被调用一次,你可以使用 assert_called_once_with() 方法,它还断言 call_count 为 1。

>>> 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):
    ...
AssertionError: Expected to be called once. Called 2 times.

assert_called_withassert_called_once_with 都对最近的调用进行断言。如果你的模拟将被调用多次,并且你希望对所有这些调用进行断言,你可以使用 call_args_list

>>> 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 帮助程序,可以轻松地对这些调用进行断言。你可以建立一个预期的调用列表,并将其与 call_args_list 进行比较。这看起来与 call_args_list 的 repr 非常相似

>>> expected = [call(1, 2, 3), call(4, 5, 6), call()]
>>> mock.call_args_list == expected
True

应对可变参数

另一种情况比较少见,但可能会让你措手不及,即你的模拟被调用时带有可变参数。 call_argscall_args_list 存储对参数的引用。如果参数被测试代码更改,那么你将无法再对模拟被调用时的值进行断言。

以下示例代码展示了这个问题。想象在“mymodule”中定义了以下函数

def frob(val):
    pass

def grob(val):
    "First frob and then clear val"
    frob(val)
    val.clear()

当我们尝试测试 grob 是否用正确的参数调用 frob 时,看看发生了什么

>>> with patch('mymodule.frob') as mock_frob:
...     val = {6}
...     mymodule.grob(val)
...
>>> val
set()
>>> mock_frob.assert_called_with({6})
Traceback (most recent call last):
    ...
AssertionError: Expected: (({6},), {})
Called with: ((set(),), {})

一种可能性是模拟复制你传入的参数。如果你进行依赖对象标识进行相等性比较的断言,这可能会导致问题。

以下是一个使用 side_effect 功能的解决方案。如果你为模拟提供 side_effect 函数,那么 side_effect 将被调用,其参数与模拟相同。这给了我们一个机会来复制参数并将它们存储起来以供以后断言。在此示例中,我使用另一个模拟来存储参数,以便我可以使用模拟方法进行断言。同样,一个帮助程序函数为我设置了这个。

>>> 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})

copy_call_args 被调用时带有将被调用的模拟。它返回一个新的模拟,我们对它进行断言。 side_effect 函数复制参数并用副本调用我们的 new_mock

注意

如果你的模拟只会被使用一次,那么有一种更简单的方法可以在调用参数时检查参数。你只需在 side_effect 函数中进行检查即可。

>>> def side_effect(arg):
...     assert arg == {6}
...
>>> mock = Mock(side_effect=side_effect)
>>> mock({6})
>>> mock(set())
Traceback (most recent call last):
    ...
AssertionError

另一种方法是创建 MockMagicMock 的子类,该子类复制(使用 copy.deepcopy())参数。以下是一个示例实现

>>> from copy import deepcopy
>>> class CopyingMock(MagicMock):
...     def __call__(self, /, *args, **kwargs):
...         args = deepcopy(args)
...         kwargs = deepcopy(kwargs)
...         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):
    ...
AssertionError: Expected call: mock({1})
Actual call: mock(set())
>>> c.foo
<CopyingMock name='mock.foo' id='...'>

当您对 MockMagicMock 进行子类化时,所有动态创建的属性以及 return_value 都将自动使用您的子类。这意味着 CopyingMock 的所有子类也将具有 CopyingMock 类型。

嵌套补丁

使用补丁作为上下文管理器很好,但如果您进行多个补丁,最终可能会出现嵌套的 with 语句,向右缩进越来越远

>>> class MyTest(unittest.TestCase):
...
...     def test_foo(self):
...         with patch('mymodule.Foo') as mock_foo:
...             with patch('mymodule.Bar') as mock_bar:
...                 with patch('mymodule.Spam') as mock_spam:
...                     assert mymodule.Foo is mock_foo
...                     assert mymodule.Bar is mock_bar
...                     assert mymodule.Spam is mock_spam
...
>>> original = mymodule.Foo
>>> MyTest('test_foo').test_foo()
>>> assert mymodule.Foo is original

使用 unittest cleanup 函数和 补丁方法:start 和 stop,我们可以在不进行嵌套缩进的情况下实现相同的效果。一个简单的帮助程序方法 create_patch,将补丁放在适当的位置并为我们返回创建的模拟

>>> 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
...         assert mymodule.Bar is mock_bar
...         assert mymodule.Spam is mock_spam
...
>>> original = mymodule.Foo
>>> MyTest('test_foo').run()
>>> assert mymodule.Foo is original

使用 MagicMock 模拟字典

您可能希望模拟字典或其他容器对象,记录对其的所有访问,同时使其仍像字典一样工作。

我们可以使用 MagicMock 来实现此目的,它将像字典一样工作,并使用 side_effect 将字典访问委托给受我们控制的真实基础字典。

当我们的 MagicMock__getitem__()__setitem__() 方法被调用(正常的字典访问)时,side_effect 会使用键(在 __setitem__ 的情况下还使用值)进行调用。我们还可以控制返回的内容。

MagicMock 被使用后,我们可以使用诸如 call_args_list 之类的属性来断言字典是如何使用的

>>> my_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

注意

使用 MagicMock 的替代方法是使用 Mock,并且提供您特别想要的魔法方法

>>> mock = Mock()
>>> mock.__getitem__ = Mock(side_effect=getitem)
>>> mock.__setitem__ = Mock(side_effect=setitem)

第三个选项是使用 MagicMock,但将 dict 作为spec(或spec_set)参数传递,以便创建的 MagicMock 仅具有可用的字典魔法方法

>>> mock = MagicMock(spec_set=dict)
>>> mock.__getitem__.side_effect = getitem
>>> mock.__setitem__.side_effect = setitem

有了这些副作用函数,mock 将表现得像一个正常的字典,但会记录访问。如果您尝试访问不存在的键,它甚至会引发 KeyError

>>> mock['a']
1
>>> mock['c']
3
>>> mock['d']
Traceback (most recent call last):
    ...
KeyError: 'd'
>>> mock['b'] = 'fish'
>>> mock['d'] = 'eggs'
>>> mock['b']
'fish'
>>> mock['d']
'eggs'

使用正常的模拟方法和属性使用它之后,您可以对访问进行断言

>>> mock.__getitem__.call_args_list
[call('a'), call('c'), call('d'), call('b'), call('d')]
>>> mock.__setitem__.call_args_list
[call('b', 'fish'), call('d', 'eggs')]
>>> my_dict
{'a': 1, 'b': 'fish', 'c': 3, 'd': 'eggs'}

模拟子类及其属性

您可能希望对 Mock 进行子类化的原因有很多。一个原因可能是添加帮助程序方法。这是一个愚蠢的示例

>>> class MyMock(MagicMock):
...     def has_been_called(self):
...         return self.called
...
>>> mymock = MyMock(return_value=None)
>>> mymock
<MyMock id='...'>
>>> mymock.has_been_called()
False
>>> mymock()
>>> mymock.has_been_called()
True

对于 Mock 实例的标准行为是,属性和返回值模拟与它们所访问的模拟类型相同。这确保了 Mock 属性是 Mocks,而 MagicMock 属性是 MagicMocks [2]。因此,如果您对子类化进行子类化以添加帮助程序方法,那么它们也将在您子类的实例的属性和返回值模拟中可用。

>>> mymock.foo
<MyMock name='mock.foo' id='...'>
>>> mymock.foo.has_been_called()
False
>>> mymock.foo()
<MyMock name='mock.foo()' id='...'>
>>> mymock.foo.has_been_called()
True

有时这很不方便。例如,一个用户 对模拟进行子类化以创建 Twisted 适配器。将此应用于属性也会导致错误。

Mock(以其所有风格)使用一个名为 _get_child_mock 的方法为属性和返回值创建这些“子模拟”。您可以通过覆盖此方法来防止您的子类用于属性。其签名是它采用任意关键字参数 (**kwargs),然后将这些参数传递给模拟构造函数

>>> class Subclass(MagicMock):
...     def _get_child_mock(self, /, **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)

使用 patch.dict 模拟导入

模拟可能变得困难的一种情况是,您在函数内部进行本地导入。这些更难模拟,因为它们没有使用我们可以修补的模块命名空间中的对象。

通常应避免本地导入。有时这样做是为了防止循环依赖,通常有更好的方法来解决问题(重构代码)或通过延迟导入来防止“前期成本”。这也可以通过比无条件本地导入更好的方式来解决(将模块存储为类或模块属性,并且仅在首次使用时进行导入)。

除此之外,有一种方法可以使用 mock 来影响导入的结果。导入从 sys.modules 字典中获取一个对象。请注意,它获取一个对象,而该对象不必是模块。首次导入模块会导致将模块对象放入 sys.modules 中,因此通常在导入某些内容时会返回一个模块。然而,情况并非总是如此。

这意味着你可以使用 patch.dict()sys.modules暂时放置一个模拟。在该修补程序处于活动状态时进行的任何导入都将获取模拟。当修补程序完成(装饰函数退出、with 语句体完成或调用 patcher.stop())时,之前存在的任何内容都将安全地恢复。

以下是一个模拟“fooble”模块的示例。

>>> import sys
>>> mock = Mock()
>>> with patch.dict('sys.modules', {'fooble': mock}):
...    import fooble
...    fooble.blob()
...
<Mock name='mock.blob()' id='...'>
>>> assert 'fooble' not in sys.modules
>>> mock.blob.assert_called_once_with()

如你所见,import fooble 成功,但在退出时,sys.modules 中没有留下“fooble”。

这也适用于 from module import name 形式

>>> mock = Mock()
>>> with patch.dict('sys.modules', {'fooble': mock}):
...    from fooble import blob
...    blob.blip()
...
<Mock name='mock.blob.blip()' id='...'>
>>> mock.blob.blip.assert_called_once_with()

通过稍微多一些的工作,你还可以模拟包导入

>>> mock = Mock()
>>> modules = {'package': mock, 'package.module': mock.module}
>>> with patch.dict('sys.modules', modules):
...    from package.module import fooble
...    fooble()
...
<Mock name='mock.module.fooble()' id='...'>
>>> mock.module.fooble.assert_called_once_with()

跟踪调用顺序和更简洁的调用断言

通过 Mock 类,你可以通过 method_calls 属性跟踪模拟对象上的方法调用的顺序。但是,这并不允许你跟踪单独模拟对象之间的调用顺序,不过我们可以使用 mock_calls 来实现相同的效果。

因为模拟在 mock_calls 中跟踪对子模拟的调用,并且访问模拟的任意属性都会创建一个子模拟,所以我们可以从父模拟创建单独的模拟。然后,对这些子模拟的调用将按顺序记录在父模拟的 mock_calls

>>> manager = Mock()
>>> mock_foo = manager.foo
>>> mock_bar = manager.bar
>>> mock_foo.something()
<Mock name='mock.foo.something()' id='...'>
>>> mock_bar.other.thing()
<Mock name='mock.bar.other.thing()' id='...'>
>>> manager.mock_calls
[call.foo.something(), call.bar.other.thing()]

然后,我们可以通过与管理器模拟上的 mock_calls 属性进行比较,对调用(包括顺序)进行断言

>>> expected_calls = [call.foo.something(), call.bar.other.thing()]
>>> manager.mock_calls == expected_calls
True

如果 patch 正在创建并放置您的模拟,那么您可以使用 attach_mock() 方法将它们附加到管理器模拟。附加后,调用将记录在管理器的 mock_calls 中。

>>> manager = 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()
<MagicMock name='mock.MockClass1().foo()' id='...'>
<MagicMock name='mock.MockClass2().bar()' id='...'>
>>> manager.mock_calls
[call.MockClass1(),
call.MockClass1().foo(),
call.MockClass2(),
call.MockClass2().bar()]

如果已执行许多调用,但您只对其中特定的序列感兴趣,那么另一种方法是使用 assert_has_calls() 方法。这需要一个调用列表(使用 call 对象构造)。如果该调用序列位于 mock_calls 中,则断言成功。

>>> 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)

即使链式调用 m.one().two().three() 不是对模拟进行的唯一调用,断言仍然成功。

有时,可能会对模拟进行多次调用,而您只对其中某些调用感兴趣。您甚至可能不在乎顺序。在这种情况下,您可以将 any_order=True 传递给 assert_has_calls

>>> 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)

更复杂的参数匹配

使用与 ANY 相同的基本概念,我们可以实现匹配器,对用作模拟参数的对象进行更复杂的断言。

假设我们希望将某个对象传递给模拟,该对象默认情况下基于对象标识进行相等比较(这是用户定义类的 Python 默认值)。要使用 assert_called_with(),我们需要传入完全相同对象。如果我们只对该对象的某些属性感兴趣,那么我们可以创建一个匹配器,它将为我们检查这些属性。

您可以在此示例中看到如何 assert_called_with 的“标准”调用是不够的

>>> class Foo:
...     def __init__(self, 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):
    ...
AssertionError: Expected: call(<__main__.Foo object at 0x...>)
Actual call: call(<__main__.Foo object at 0x...>)

我们 Foo 类的比较函数可能如下所示

>>> def compare(self, other):
...     if not 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)
...

将所有这些放在一起

>>> match_foo = Matcher(compare, Foo(1, 2))
>>> mock.assert_called_with(match_foo)

使用我们的比较函数和我们要比较的 Foo 对象实例化 Matcher。在 assert_called_with 中,将调用 Matcher 相等方法,该方法将模拟调用的对象与我们用它创建匹配器对象的对象进行比较。如果它们匹配,则 assert_called_with 通过,如果不匹配,则会引发 AssertionError

>>> match_wrong = Matcher(compare, Foo(3, 4))
>>> mock.assert_called_with(match_wrong)
Traceback (most recent call last):
    ...
AssertionError: Expected: ((<Matcher object at 0x...>,), {})
Called with: ((<Foo object at 0x...>,), {})

通过一些调整,您可以让比较函数直接引发 AssertionError 并提供更有用的失败消息。

从 1.5 版本开始,Python 测试库 PyHamcrest 提供了类似的功能,可能在这里有用,以其相等匹配器 (hamcrest.library.integration.match_equality) 的形式。