unittest.mock-入门

版本 3.3 中的新Function。

Using Mock

模拟修补方法

Mock对象的常见用法包括:

  • Patching methods

  • 记录对象的方法调用

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

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

一旦使用了我们的模拟程序(在此示例中为real.method),它便具有使您可以 assert 其使用方式的方法和属性。

Note

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

调用该模拟后,其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方法。如果pass对象调用它,则它在其上调用close

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

因此,要对其进行测试,我们需要使用close方法传递一个对象并检查其是否被正确调用。

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

我们无需做任何工作即可在我们的模拟中提供“关闭”方法。访问 close 会创建它。因此,如果尚未调用“ close”,则在测试中访问它会创建它,但是assert_called_with()会引发失败异常。

Mocking Classes

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

在下面的示例中,我们有一个函数some_function实例化Foo并在其上调用一个方法。对patch()的调用用模拟代替了Foo类。 Foo实例是调用模拟的结果,因此可以pass修改模拟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'

命名您的模拟

给您的模拟名称起一个有用的作用。该名称显示在该模拟的代表中,当该模拟出现在测试失败消息中时可能会有所帮助。该名称还会传播到模拟的属性或方法:

>>> 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进行 assert,并且调用了任何意外方法,则 assert 将失败。这很有用,因为除了 assert 您所期望的呼叫已经发出之外,您还要检查它们是否以正确的 Sequences 进行并且没有其他呼叫:

您使用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()的呼叫将我们的呼叫对象转变为代表链接呼叫的呼叫列表。

pass模拟引发异常

有用的属性是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支持模拟Asynchronous Iterators__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]

模拟异步上下文 Management 器

从 Python 3.8 开始,AsyncMockMagicMock支持pass__aenter____aexit__模拟异步上下文 Management 器。默认情况下,__aenter____aexit__是返回异步Function的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,那么即使您的代码现在已损坏,您的测试也将 continue pass!

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)

如果您希望这种更智能的匹配也可以与模拟中的方法调用一起使用,则可以使用auto-speccing

如果您想要一种更强大的规范形式,以防止设置任意属性以及获取任意属性,则可以使用* spec_set 而不是 spec *。

Patch Decorators

Note

对于patch(),重要的是您要在查找对象的名称空间中修补对象。这通常很简单,但是要获得快速指南,请阅读在哪里打补丁

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

模拟为此提供了三个方便的修饰符: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"

如果需要,模块名称可以用“ __”形式加“点”:

>>> @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()与一个参数一起使用(或patch.object()与两个参数一起使用)。模拟将为您创建并传递到测试函数/方法中:

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

当您嵌套补丁装饰器时,模拟将以它们应用的 Sequences(装饰器的正常* Python *Sequences)传递给装饰的函数。这意味着从下至上,因此在上面的示例中,首先传入了test_module.ClassName2的模拟。

还有一个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都可以用作上下文 Management 器。

在使用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”开头的每个方法相同。

Further Examples

这是一些稍微高级的方案的更多示例。

模拟链接的呼叫

一旦了解了return_value属性,就可以使用模拟方法直接模拟链接的调用。首次调用模拟时,或者您在调用其return_value之前先获取它,则会创建一个新的Mock

这意味着您可以pass查询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)

从这里开始,这是一个简单的步骤,可以配置然后对链接的调用进行 assert。当然,另一种选择是首先以更可测试的方式编写代码……

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

>>> 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是否以正确的方式使用了响应对象。

由于此调用链是pass实例属性进行的,因此我们可以在Something实例上猴子修补_属性。在这种特殊情况下,我们只对最后一次调用start_call的返回值感兴趣,因此我们没有太多配置要做。让我们假设它返回的对象是“类似文件的”,那么我们将确保响应对象使用内置的open()作为其spec

为此,我们创建一个模拟实例作为模拟后端,并为其创建模拟响应对象。要将响应设置为该finallystart_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)

有了这些,我们就可以在适当的地方修补“模拟后端”,并可以进行 true 的调用:

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

使用mock_calls,我们可以使用单个 assert 检查链接的调用。链接调用是一行代码中的多个调用,因此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

Partial mocking

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

我找到了一种简单的方法,其中包括使用模拟有效地包装 date 类,但将对构造函数的调用传递给真实类(并返回真实实例)。

此处使用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 生成器是在[1]上迭代时使用yield语句返回一系列值的函数或方法。

调用生成器方法/函数以返回生成器对象。然后是生成器对象被迭代。迭代的协议方法为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]

将相同的补丁程序应用于每种测试方法

如果要为多种测试方法准备好多个补丁,那么显而易见的方法是将补丁装饰器应用于每种方法。感觉就像不必要的重复。对于 Python 2.6 或更高版本,您可以使用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'

Management 补丁的另一种方法是使用补丁方法:启动和停止。这些使您可以将修补程序移到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()

如果您使用此技术,则必须pass调用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 作为第一个参数传递,因为我想 assert 哪些对象正在调用此特定方法。问题是您不能为此使用模拟程序进行修补,因为如果用模拟程序替换未绑定的方法,则从实例中获取时它不会成为绑定方法,因此不会自我传递。解决方法是改用实际函数修补未绑定的方法。 patch()装饰器使pass模拟修补方法变得如此简单,以至于不得不创建一个实函数变得很麻烦。

如果您将autospec=True传递给补丁,则它将使用* real *函数对象进行补丁。该函数对象与要替换的对象具有相同的签名,但在幕后委托给模拟对象。您仍然可以pass与以前完全相同的方式来自动创建模拟。但是,它的意思是,如果您使用它来修补类上的未绑定方法,那么如果从实例中获取该模拟函数,它将被转换为绑定方法。它将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调用该方法。

pass模拟检查多个呼叫

模拟有一个不错的 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()方法,该方法还 assertcall_count是一个。

>>> 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都对最近调用进行 assert。如果您的模拟将被多次调用,并且您想对所有*这些调用进行 assert,则可以使用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的代表非常相似:

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

处理可变参数

另一种情况是罕见的,但可能会叮咬您,即使用可变参数调用模拟程序。 call_argscall_args_list存储对参数的引用。如果参数被测试代码所突变,那么您将无法再 assert 在调用模拟时的值。

这是一些显示问题的示例代码。想象一下“ mymodule”中定义的以下Function:

def frob(val):
    pass

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

当我们try测试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_effectFunction的解决方案。如果为模拟提供side_effect函数,则将使用与模拟相同的 args 调用side_effect。这为我们提供了复制参数并将其存储以供以后语句的机会。在此示例中,我使用* another *模拟来存储参数,以便可以使用模拟方法来进行 assert。同样,一个辅助函数为我设置了此Function。

>>> 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与将被调用的模拟一起调用。它返回一个新的模拟,我们对其进行 assert。 side_effect函数复制 args 并用副本调用new_mock

Note

如果您的模拟只使用一次,则有一种更简便的方法可以在调用它们时检查参数。您只需在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(CopyingMock, self).__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类型。

Nesting Patches

将 patch 用作上下文 Management 器是很好的方法,但是如果您执行多个补丁,finally可能会嵌套嵌套越来越向右缩进的语句:

>>> 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函数和补丁方法:启动和停止,我们可以在没有嵌套缩进的情况下实现相同的效果。一个简单的辅助方法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将字典访问权限委派给我们控制下的真实基础字典。

当我们的MagicMockgetitem()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

Note

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

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

使用这些副作用Function后,mock的行为将像普通字典一样,但会记录访问情况。如果您try访问不存在的密钥,它甚至会引发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'

使用完之后,您可以使用普通的模拟方法和属性对访问进行 assert:

>>> 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属性是MocksMagicMock属性是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

有时这很不方便。例如,one user正在将模拟子类化为Twisted adaptor。将其应用于属性实际上也会导致错误。

Mock(尽其所能)使用一种名为_get_child_mock的方法来为属性和返回值创建这些“子模拟”。您可以pass重写此方法来防止子类用于属性。签名是它接受任意关键字参数(**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)
  • [2]
    • 此规则的 exception 是不可调用的模拟。属性使用可调用的变体,因为否则不可调用的模拟不能具有可调用的方法。

使用 patch.dict 模拟导入

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

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

除此之外,还有一种使用mock来影响导入结果的方法。导入会从sys.modules字典中获取* object 。注意,它获取一个 object *,它不一定是一个模块。首次导入模块会导致将模块对象放入 sys.modules 中,因此通常在导入某些内容时会得到模块。但是,不必是这种情况。

这意味着您可以使用patch.dict()临时*在sys.modules中放置一个模拟。在此修补程序处于活动状态时的任何导入都将获取该模拟。补丁完成后(修饰的函数退出,with 语句主体完成或调用patcher.stop()),那么以前存在的内容将被安全地恢复。

这是一个模拟“愚蠢”模块的示例。

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

pass更多的工作,您还可以模拟包导入:

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

跟踪呼叫的 Sequences 和较少详细的呼叫 assert

Mock类可让您passmethod_calls属性跟踪模拟对象的方法调用的“Sequences”。这不允许您跟踪单独的模拟对象之间的调用 Sequences,但是我们可以使用mock_calls来达到相同的效果。

因为模拟跟踪mock_calls中对子模拟的调用,并且访问模拟的任意属性会创建一个子模拟,所以我们可以从父模拟中创建单独的模拟。然后,对这些子模拟的调用将全部按 Sequences 记录在父的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()]

然后,我们可以pass与 Management 器模拟中的mock_calls属性进行比较来 assert 包括调用的调用:

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

如果patch正在创建并将模拟放置到位,则可以使用attach_mock()方法将它们附加到 Manager 模拟。连接后,通话将被记录在 Management 员的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()]

如果进行了许多调用,但是您只对它们的特定 Sequences 感兴趣,那么另一种方法是使用assert_has_calls()方法。这将获取一个调用列表(由call对象构造)。如果该调用序列在mock_calls中,则 assert 成功。

>>> 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(),该 assert 仍然成功。

有时候,一个模拟可能会对其进行多次调用,而您只对 assert 其中的一些*感兴趣。您甚至可能不在乎订单。在这种情况下,您可以将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相同的基本概念,我们可以实现匹配器,以对用作模拟参数的对象进行更复杂的 assert。

假设我们期望将某个对象传递给模拟对象,该对象默认情况下会根据对象身份比较相等(这是用户定义类的 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)

Matcher使用我们的 compare 函数和要与之比较的Foo对象实例化。在assert_called_with中,将调用Matcher相等方法,该方法将调用模拟对象的对象与创建匹配器的对象进行比较。如果匹配,则assert_called_withpass,如果不匹配,则引发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...>,), {})

pass一些调整,您可以使比较Function直接提高AssertionError并提供更有用的故障消息。

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