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
),它就具有允许你对它的使用方式进行断言的方法和属性。
一旦模拟对象被调用,它的 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()
的调用会将类 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 = 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 起,AsyncMock
和 MagicMock
支持通过 __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 起,AsyncMock
和 MagicMock
支持通过 __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()
从现有对象创建 Mock¶
过度使用 mock 的一个问题是,它将你的测试与 mock 的实现耦合,而不是与你的实际代码耦合。假设你有一个实现了 some_method
的类。在另一个类的测试中,你为此对象提供了一个 mock,该 mock *也* 提供了 some_method
。如果稍后你重构了第一个类,使其不再具有 some_method
,那么即使你的代码现在已损坏,你的测试仍将继续通过!
Mock
允许你使用 spec 关键字参数提供一个对象作为 mock 的规范。访问 mock 上不存在于你的规范对象中的方法/属性会立即引发属性错误。如果你更改了规范的实现,那么使用该类的测试将立即开始失败,而无需你在这些测试中实例化该类。
>>> mock = Mock(spec=SomeClass)
>>> mock.old_method()
Traceback (most recent call last):
...
AttributeError: Mock object has no attribute 'old_method'. Did you mean: 'class_method'?
使用规范还可以更智能地匹配对 mock 的调用,而不管某些参数是作为位置参数还是命名参数传递的
>>> 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)
如果你希望这种更智能的匹配也适用于 mock 上的方法调用,可以使用自动规范。
如果你想要更强的规范形式,既可以防止设置任意属性,也可以防止获取任意属性,则可以使用 spec_set 代替 spec。
使用 side_effect 返回每个文件的内容¶
mock_open()
用于修补 open()
方法。side_effect
可用于每次调用返回一个新的 Mock 对象。这可用于返回存储在字典中的每个文件的不同内容
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()
(或有两个参数的 patch.object()
)。该 mock 将为你创建并传递给测试函数/方法
>>> class MyTest(unittest.TestCase):
... @patch.object(SomeClass, 'static_method')
... def test_something(self, mock_method):
... SomeClass.static_method()
... mock_method.assert_called_with()
...
>>> MyTest('test_something').test_something()
你可以使用此模式堆叠多个 patch 装饰器
>>> 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()
当你嵌套 patch 装饰器时,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
patch
、patch.object
和 patch.dict
都可以用作上下文管理器。
在你使用 patch()
为你创建一个 mock 的地方,你可以使用 with 语句的“as”形式来获取对该 mock 的引用
>>> 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)
作为替代,patch
、patch.object
和 patch.dict
可以用作类装饰器。以这种方式使用时,它与将装饰器单独应用于每个名称以“test”开头的方法相同。
更多示例¶
以下是一些针对稍微更高级场景的更多示例。
模拟链式调用¶
一旦你了解了 return_value
属性,使用 mock 模拟链式调用实际上很简单。当第一次调用 mock 时,或者在你调用之前获取其 return_value
时,将创建一个新的 Mock
。
这意味着你可以通过查询 return_value
mock 来查看对 mock 对象的调用返回的对象是如何被使用的
>>> 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
实例上 monkey-patch backend
属性。在这种特定情况下,我们只对最后一次调用 start_call
的返回值感兴趣,因此我们没有太多配置要做。让我们假设它返回的对象是“类似文件的”,因此我们将确保我们的响应对象使用内置的 open()
作为其 spec
。
为此,我们创建一个 mock 实例作为我们的 mock 后端,并为其创建一个 mock 响应对象。要将响应设置为最后一次 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)
有了这些,我们可以在适当的位置 monkey-patch “mock 后端”,并可以进行实际调用
>>> 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 编写的,因此我无法仅 monkey-patch 静态 datetime.date.today()
方法。
我找到了一种简单的方法来做到这一点,该方法有效地使用 mock 包裹 date 类,但将对构造函数的调用传递给实际类(并返回实际实例)。
这里使用 patch 装饰器
来模拟被测模块中的 date
类。然后,将模拟的 date 类的 side_effect
属性设置为返回实际日期的 lambda 函数。当调用模拟的 date 类时,将构造一个实际的日期,并由 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(...)
构造函数的调用仍然会返回正常的日期。如果没有这个,您可能会发现自己必须使用与被测代码完全相同的算法来计算预期结果,这是一个典型的测试反模式。
对 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'
管理补丁的另一种方法是使用 补丁方法:start 和 stop。这些允许您将补丁移动到您的 setUp
和 tearDown
方法中。
>>> 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 有一个很好的 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 'foo_bar' to be called once. Called 2 times.
Calls: [call('baz', spam='eggs'), call()].
assert_called_with
和 assert_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_args
和 call_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
另一种方法是创建 Mock
或 MagicMock
的子类,该子类复制(使用 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 not found.
Expected: mock({1})
Actual: mock(set())
>>> c.foo
<CopyingMock name='mock.foo' id='...'>
当您对 Mock
或 MagicMock
进行子类化时,所有动态创建的属性以及 return_value
将自动使用您的子类。这意味着 CopyingMock
的所有子级也将具有 CopyingMock
类型。
嵌套补丁¶
使用 patch 作为上下文管理器很方便,但如果执行多个 patch,最终可能会导致嵌套的 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
函数和 patch 方法:start 和 stop,我们可以实现相同的效果,而无需嵌套缩进。一个简单的辅助方法 create_patch
可以设置 patch,并返回为我们创建的 mock 对象。
>>> 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__()
方法时(正常的字典访问),会使用键(并且在 __setitem__
的情况下还会使用值)调用 side_effect
。我们还可以控制返回的内容。
在使用 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
注意
使用 Mock
的一个替代方法是使用 MagicMock
,并仅提供您明确想要的魔法方法。
>>> 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 方法和属性来断言访问情况。
>>> 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 子类及其属性¶
您可能有各种原因想要继承 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
属性是 Mocks
,而 MagicMock
属性是 MagicMocks
[2]。因此,如果您正在继承以添加辅助方法,那么它们也将可在您子类的实例的属性和返回值 mock 上使用。
>>> 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
有时这很不方便。例如,一位用户 正在继承 mock 来创建一个 Twisted 适配器。将其应用于属性实际上会导致错误。
Mock
(及其所有变体)使用一种称为 _get_child_mock
的方法来为属性和返回值创建这些“子 mock”。您可以通过覆盖此方法来防止您的子类用于属性。其签名是它接受任意关键字参数 (**kwargs
),然后将其传递给 mock 构造函数。
>>> 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)
此规则的一个例外是不可调用的 mock。属性使用可调用变体,因为否则不可调用的 mock 将无法具有可调用的方法。
使用 patch.dict 模拟导入¶
难以模拟的一种情况是在函数内部有本地导入。这些更难以模拟,因为它们没有使用我们可以 patch 掉的模块命名空间中的对象。
通常应避免本地导入。有时这样做是为了防止循环依赖,对于这种情况,通常有更好的方法来解决问题(重构代码)或通过延迟导入来防止“前期成本”。也可以通过比无条件本地导入更好的方式来解决此问题(将模块存储为类或模块属性,仅在首次使用时导入)。
除此之外,有一种方法可以使用 mock
来影响导入的结果。导入从 sys.modules
字典中获取对象。请注意,它获取的是一个对象,该对象不必是模块。首次导入模块会在 sys.modules
中放置一个模块对象,因此通常在导入某些内容时会返回一个模块。但情况并非必须如此。
这意味着您可以使用 patch.dict()
在 sys.modules
中临时放置一个 mock。当此 patch 处于活动状态时进行的任何导入都将获取该 mock。当 patch 完成时(装饰的函数退出、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 对象上方法调用的顺序。但是,这不允许您跟踪不同 mock 对象之间调用的顺序,但是我们可以使用 mock_calls
来实现相同的效果。
由于 mock 在 mock_calls
中跟踪对子 mock 的调用,并且访问 mock 的任意属性会创建一个子 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 上的 mock_calls
属性进行比较来断言调用,包括顺序。
>>> expected_calls = [call.foo.something(), call.bar.other.thing()]
>>> manager.mock_calls == expected_calls
True
如果 patch
正在创建并放置您的 mock,则可以使用 attach_mock()
方法将它们附加到管理器 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()
不是对 mock 进行的唯一调用,断言仍然成功。
有时,可能会对 mock 进行多次调用,而您只对其中某些调用进行断言。您甚至可能不关心顺序。在这种情况下,您可以将 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
相同的基本概念,我们可以实现匹配器,对用作 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 not found.
Expected: mock(<__main__.Foo object at 0x...>)
Actual: mock(<__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
使用我们的比较函数和我们想要比较的 Foo
对象进行实例化。 在 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)。