26.6. unittest.mock - 开始

版本3.3中的新功能。

26.6.1. Using Mock

26.6.1.1. Mock Patching Methods

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)

26.6.1.2. Mock for Method Calls on an Object

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

下面的简单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()会引发一个失败异常。

26.6.1.3. Mocking Classes

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

在下面的例子中,我们有一个函数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'

26.6.1.4. Naming your mocks

它可以是有用的给你的嘲笑一个名字。该名称显示在模拟的repr中,并且当模拟出现在测试失败消息中时可以是有帮助的。该名称还传播到模拟的属性或方法:

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

26.6.1.5. Tracking all Calls

通常你想跟踪一个方法的多个调用。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

26.6.1.6. Setting Return Values and Attributes

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

>>> 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()的调用,它将我们的调用对象转换为表示链接调用的调用列表。

26.6.1.7. Raising exceptions with mocks

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

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

26.6.1.8. Side effect functions and iterables

side_effect也可以设置为一个函数或一个迭代。side_effect作为可迭代的用例是,你的mock将被调用几次,你想让每次调用返回一个不同的值。当将side_effect设置为可迭代时,每次对mock的调用都会返回可迭代的下一个值:

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

26.6.1.9. Creating a Mock from an Existing Object

过度使用模拟的一个问题是它将你的测试与你的mock的实现相结合,而不是你的实际代码。假设你有一个实现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)

如果您希望此更智能的匹配也可以使用模拟中的方法调用,您可以使用auto-speccing

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

26.6.2. Patch Decorators

注意

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

如果你想使用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()

当你嵌套补丁修饰符时,mock会以它们应用的相同顺序传递到修饰函数中(应用修饰符的正常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可以用作类装饰器。当以这种方式使用时,它与将装饰器单独应用于其名称以“测试”开始的每个方法相同。

26.6.3. Further Examples

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

26.6.3.1. Mocking chained calls

如果您理解return_value属性,则模拟链接调用实际上是直接的。当第一次调用模拟或在调用之前获取return_value时,会创建一个新的Mock

这意味着您可以查看从调用模拟对象返回的对象是否已通过查询return_value 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()具体来说,我们要测试代码部分 更多 代码以正确的方式使用响应对象。

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

为此,我们创建一个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)

有了这些,我们猴子修补“模拟后端”到位,并可以使真正的电话:

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

26.6.3.2. Partial mocking

在一些测试中,我想对datetime.date.today()进行一次调用,以返回一个已知的日期,但我不想阻止被测试的代码创建新的日期对象。不幸的是,datetime.date是用C写的,所以我不能只是monkey-patch出静态的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请参阅where to patch

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

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

此博客条目中讨论了处理模拟日期或其他内置类的替代方法。

26.6.3.3. Mocking a Generator Method

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]还有生成器表达式和生成器的更多高级用法,但我们在这里不关心它们。对生成器非常好的介绍以及它们的强大功能是:生成器对系统程序员的技巧

26.6.3.4. Applying the same patch to every test method

如果你想要多个测试方法的地方的几个补丁,明显的方法是应用补丁修饰符到每个方法。这可以感觉不必要的重复。对于Python 2.6或更高版本,你可以使用patch()(在所有的各种形式)作为类装饰器。这将修补程序应用于类上的所有测试方法。测试方法由名称以test开头的方法标识:

>>> @patch('mymodule.SomeClass')
... class MyTest(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 methods: start and stop这些允许您将修补移动到setUptearDown方法中。

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

26.6.3.5. Mocking Unbound Methods

今天写测试时,我需要修补未绑定的方法(在类上修补方法,而不是实例)。我需要self作为第一个参数传递,因为我想断言关于哪些对象调用这个特定的方法。问题是,你不能用一个嘲笑补丁,因为如果你用一个模拟替换一个未绑定的方法,它不会成为一个绑定的方法,当从实例获取,所以它不会自我传递。解决方法是使用实​​函数修补未绑定的方法。patch()装饰器使得使用模拟来修改方法变得如此简单,以至于必须创建一个真实的函数变成一个讨厌的东西。

如果你将autospec=True传递给patch,那么它会使用真实函数对象进行修补。此函数对象具有与其正在替换的声明相同的声明,但委派给在引擎盖下的模拟。你仍然得到你的模拟自动创建完全相同的方式,以前。它的意思是,如果你使用它来修补一个类的未绑定的方法,mocked函数将变成一个绑定的方法,如果它是从实例获取。它会将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来调用。

26.6.3.6. Checking multiple calls with 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是一个。

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

26.6.3.7. Coping with mutable arguments

另一种情况是罕见的,但可以咬你,是当你的模拟被调用与可变参数。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将使用与模拟相同的参数调用。这使我们有机会复制参数并存储它们以供以后的断言。在这个例子中,我使用另一个 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函数创建args的副本,并使用副本调用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

26.6.3.8. Nesting Patches

使用补丁作为上下文管理器是很好的,但如果你做了多个补丁,你可以结束嵌套语句进一步向右缩进:

>>> class MyTest(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 methods: start and stop,我们可以实现相同的效果,没有嵌套的缩进。一个简单的帮助方法,create_patch,将补丁放在原处,并返回为我们创建的模拟:

>>> class MyTest(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

26.6.3.9. Mocking a dictionary with 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, 'c': 3, 'b': 'fish', 'd': 'eggs'}

26.6.3.10. Mock subclasses and their attributes

您可能想要将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实例的标准行为是属性和返回值嘲笑的类型与其访问的模拟类型相同。This ensures that Mock attributes are Mocks and MagicMock attributes are 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

有时这是不方便的。例如,一个用户是对模拟进行子类化以创建扭曲适配器将此应用于属性实际上导致错误。

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不能有可调用的方法。

26.6.3.11. Mocking imports with patch.dict

一种情况是嘲笑可能很困难,你有一个函数内部的本地导入。这些更难以模拟,因为他们没有使用我们可以修补的模块命名空间中的对象。

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

除此之外,有一种方法使用mock来影响导入的结果。导入从sys.modules字典中提取对象请注意,它获取一个对象,这不需要是一个模块。首次导入模块会导致将模块对象放在sys.modules中,因此通常在导入某个模块时,您会得到一个模块。然而,这不是必须的。

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

下面是一个模拟“fooble”模块的例子。

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

这也适用于 模块 导入 名称

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

26.6.3.12. Tracking order of calls and less verbose call assertions

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

因为mock跟踪在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中,则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()不是对模拟器的唯一调用,该断言仍然成功。

有时,一个模拟可能有几个调用,并且你只是对关于一些的调用感兴趣。你甚至可能不关心订单。在这种情况下,您可以将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)

26.6.3.13. More complex argument matching

使用与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(<__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对象来实例化Matcherassert_called_with中,将调用Matcher等式方法,该方法将mock调用的对象与我们创建的匹配器进行比较。如果它们匹配,则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