unittest.mock ---开始

3.3 新版功能.

使用模拟

模拟修补方法

常用用途 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”,那么在测试中访问它将创建它,但是 assert_called_with() 将引发失败异常。

嘲弄课

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

在下面的示例中,我们有一个函数 some_function 例示 Foo 并对其调用方法。呼唤 patch() 替换类 Foo 嘲弄地模仿这个 Foo 实例是调用模拟的结果,因此通过修改模拟来配置它。 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的属性或方法:

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

但是,不会记录返回mock的调用的参数,这意味着在用于创建祖先的参数很重要的情况下,不可能跟踪嵌套调用:

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

副作用函数和Iterables

side_effect 也可以设置为函数或iterable。的用例 side_effect 作为一个iterable,您的mock将被多次调用,并且您希望每个调用返回不同的值。当你设置 side_effect 对于iterable,对mock的每个调用都会从iterable返回下一个值:

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

对于更高级的用例,比如根据mock的调用动态地改变返回值, side_effect 可以是函数。将使用与mock相同的参数调用函数。函数返回的是调用返回的:

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

模拟异步迭代器

从Python3.8开始, AsyncMockMagicMock 支持嘲笑 异步迭代器 通过 __aiter__ . 这个 return_value 属性 __aiter__ 可用于设置要用于迭代的返回值。

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

模拟异步上下文管理器

从Python3.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 . 在另一个类的测试中,您提供了这个对象的模拟 also 提供 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)

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

如果您想要一种更强大的规范形式来阻止任意属性的设置和获取,那么您可以使用 spec_set 而不是 spec .

补丁装饰器

注解

patch() 在查找对象的名称空间中修补对象很重要。这通常很简单,但为了快速阅读 where to 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

如果你想用模型修补,你可以使用 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()

当嵌套修补程序装饰器时,模拟将按它们应用的相同顺序传入装饰函数(正常 Python 命令应用装饰器)。这意味着从下到上,因此在上面的示例中, 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 可以全部用作上下文管理器。

你在哪里使用 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 可以用作类修饰器。当以这种方式使用时,它与将decorator单独应用于名称以“test”开头的每个方法相同。

其他示例

下面是一些更高级的场景的更多示例。

模拟链接调用

一旦您理解了 return_value 属性。当一个模拟被第一次调用时,或者你把它 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 以正确的方式使用响应对象。

由于此调用链是由实例属性发出的,因此我们可以对 backend 属性上的 Something 实例。在这种特殊情况下,我们只对 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语言写的,所以我不能仅仅用猴子来修补静态的 date.today() 方法。

我发现了一种简单的方法,它包括用一个mock有效地封装日期类,但将调用传递给构造函数到实际类(并返回实际实例)。

这个 patch decorator 在这里用来模拟 date 在测试模块中初始化。这个 side_effect 然后将mock-date类的属性设置为返回实数的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 在模块中, uses 它。见 where to patch .

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

对日期构造函数的调用记录在 mock_date 属性 (call_count 和朋友)这对你的测试也很有用。

中讨论了处理模拟日期或其他内置类的另一种方法。 this blog entry .

模拟生成器方法

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

还有生成器表达式等等 advanced uses 但是我们不关心生成器。非常好地介绍了生成器及其功能: Generator Tricks for Systems Programmers .

对每个测试方法应用相同的补丁

如果您想要为多个测试方法准备好几个补丁,最明显的方法是将补丁装饰器应用到每个方法。这感觉像是不必要的重复。对于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'

管理补丁的另一种方法是使用 修补方法:启动和停止 . 这些可以让你把补丁移到你的 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 . 这可能比您想象的更麻烦,因为如果在设置中引发异常,则不会调用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作为第一个参数传入,因为我想断言哪些对象正在调用这个特定的方法。问题是,您不能为此使用模拟修补,因为如果用模拟替换未绑定的方法,则从实例中提取时,它不会成为绑定方法,因此它不会自我传入。解决方法是用实函数来修补未绑定的方法。这个 patch() decorator使得用一个mock修补方法变得非常简单,以至于必须创建一个真正的函数成为一个麻烦。

如果你通过 autospec=True 然后它用一个 real 函数对象。这个函数对象与它要替换的对象具有相同的签名,但是委托给了一个位于引擎盖下的模拟对象。您仍然可以使用与以前完全相同的方式创建模拟自动。但是,它的意思是,如果使用它来修补类上的未绑定方法,那么如果从实例中提取模拟函数,它将变成绑定方法。它将拥有 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 然后用一个模拟实例修补未绑定的方法,而不是用 self .

使用模拟检查多个调用

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 是一个。

>>> 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最近的 调用。如果你的模拟会被多次调用,而你想对 all 你可以用的调用 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 helper使断言这些调用变得容易。您可以建立一个预期调用的列表,并将其与 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 商店 参考文献 去参数。如果被测试的代码改变了参数,那么当调用mock时,就不能再断言值是什么。

下面是一些显示问题的示例代码。想象一下“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 将使用与mock相同的参数调用。这给了我们一个机会来复制这些参数,并将它们存储起来,以供以后的断言使用。在这个例子中,我使用 另一个 mock存储参数,这样我就可以使用mock方法进行断言。另一个助手函数为我设置了这个。::

>>> from copy import deepcopy
>>> from unittest.mock import Mock, patch, DEFAULT
>>> def copy_call_args(mock):
...     new_mock = Mock()
...     def side_effect(*args, **kwargs):
...         args = deepcopy(args)
...         kwargs = deepcopy(kwargs)
...         new_mock(*args, **kwargs)
...         return DEFAULT
...     mock.side_effect = side_effect
...     return new_mock
...
>>> with patch('mymodule.frob') as mock_frob:
...     new_mock = copy_call_args(mock_frob)
...     val = {6}
...     mymodule.grob(val)
...
>>> new_mock.assert_called_with({6})
>>> new_mock.call_args
call({6})

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

嵌套修补程序

将patch用作上下文管理器是很好的,但是如果您执行多个修补程序,则可以以嵌套的语句结尾,语句进一步向右缩进:

>>> 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 ,其行为将类似于字典,并使用 side_effect 委托字典访问我们控制的真正的基础字典。

__getitem__()__setitem__() 我们的方法 MagicMock 则称为(普通字典访问) 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 是使用 Mockonly 提供你特别想要的魔法方法:

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

A 第三的 选项是使用 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与访问它们的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 子类化mock以创建 Twisted adaptor . 将此应用于属性也会导致错误。

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

此规则的一个例外是不可调用的模拟。属性使用可调用变量,因为否则不可调用mock不能有可调用方法。

使用patch.dict模拟导入

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

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

除此之外,还有一种使用方法 mock 影响导入的结果。导入获取 objectsys.modules 字典。注意,它获取一个 object ,不必是模块。首次导入模块会导致将模块对象放入 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 成功了,但在出口处没有留下“fooble” sys.modules .

这也适用于 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 达到同样的效果。

因为mocks跟踪对子项的调用mocks in mock_calls 访问一个mock的任意属性会创建一个子mock,我们可以从父mock创建单独的mock。然后,所有对这些模仿子项的调用将按顺序记录在 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() 并不是唯一对模拟进行调用,断言仍然成功。

有时,一个模拟可能有几个调用,你只对断言感兴趣 some 这些调用。你甚至可能不关心订单。在这种情况下,你可以通过 any_order=Trueassert_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 我们可以实现matcher来对用作模拟参数的对象进行更复杂的断言。

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

如果matcher对象可以使用类似这样的比较函数进行相等操作,那么它的外观应该是这样的:

>>> 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 用我们的比较函数和 Foo 我们要比较的对象。在 assert_called_with 这个 Matcher 将调用相等方法,该方法将mock调用的对象与创建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) .