unittest.mock —- 上手指南

3.3 新版功能.

使用 mock

模拟方法调用

使用 Mock 的常见场景:

  • 模拟函数调用

  • 记录在对象上的方法调用

你可能需要替换一个对象上的方法,用于确认此方法被系统中的其他部分调用过,并且调用时使用了正确的参数。

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

使用了 mock (本例中的 real.method) 之后,它有方法和属性可以让你针对它是被如何使用的下断言。

备注

在多数示例中,MockMagicMock 两个类可以相互替换,而 MagicMock 是一个更适用的类,通常情况下,使用它就可以了。

如果 mock 被调用,它的 called 属性就会变成 True,更重要的是,我们可以使用 assert_called_with() 或者 assert_called_once_with() 方法来确认它在被调用时使用了正确的参数。

在如下的测试示例中,验证对于 ProductionClass().method 的调用会导致 something 的调用。

  1. >>> class ProductionClass:
  2. ... def method(self):
  3. ... self.something(1, 2, 3)
  4. ... def something(self, a, b, c):
  5. ... pass
  6. ...
  7. >>> real = ProductionClass()
  8. >>> real.something = MagicMock()
  9. >>> real.method()
  10. >>> real.something.assert_called_once_with(1, 2, 3)

对象上的方法调用的 mock

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

下面这个简单的 ProductionClass 具有一个 closer 方法。 如果它附带一个对象被调用那么它就会调用其中的 close

  1. >>> class ProductionClass:
  2. ... def closer(self, something):
  3. ... something.close()
  4. ...

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

  1. >>> real = ProductionClass()
  2. >>> mock = Mock()
  3. >>> real.closer(mock)
  4. >>> mock.close.assert_called_with()

我们不需要做任何事来在我们的 mock 上提供 ‘close’ 方法。 访问 close 的操作就会创建它。 因此,如果 ‘close’ 还未被调用那么在测试时访问它就将创建它,但是 assert_called_with() 则会引发一个失败的异常。

模拟类

一个常见的用例是模拟被测试的代码所实例化的类。 当你给一个类打上补丁,该类就会被替换为一个 mock。 实例是通过 调用该类 来创建的。 这意味着你要通过查看被模拟类的返回值来访问“mock 实例”。

在下面的例子中我们有一个函数 some_function 实例化了 Foo 并调用该实例中的一个方法。 对 patch() 的调用会将类 Foo 替换为一个 mock。 Foo 实例是调用该 mock 的结果,所以它是通过修改 return_value 来配置的。

  1. >>> def some_function():
  2. ... instance = module.Foo()
  3. ... return instance.method()
  4. ...
  5. >>> with patch('module.Foo') as mock:
  6. ... instance = mock.return_value
  7. ... instance.method.return_value = 'the result'
  8. ... result = some_function()
  9. ... assert result == 'the result'

命名你的 mock

给你的 mock 起个名字可能会很有用。 名字会显示在 mock 的 repr 中并在 mock 出现于测试失败消息中时可以帮助理解。 这个名字也会被传播给 mock 的属性或方法:

  1. >>> mock = MagicMock(name='foo')
  2. >>> mock
  3. <MagicMock name='foo' id='...'>
  4. >>> mock.method
  5. <MagicMock name='foo.method' id='...'>

追踪所有的调用

通常你会想要追踪对某个方法的多次调用。 mock_calls 属性记录了所有对 mock 的子属性的调用 —— 并且还包括对它们的子属性的调用。

  1. >>> mock = MagicMock()
  2. >>> mock.method()
  3. <MagicMock name='mock.method()' id='...'>
  4. >>> mock.attribute.method(10, x=53)
  5. <MagicMock name='mock.attribute.method()' id='...'>
  6. >>> mock.mock_calls
  7. [call.method(), call.attribute.method(10, x=53)]

如果你做了一个有关 mock_calls 的断言并且有任何非预期的方法被调用,则断言将失败。 这很有用处,因为除了断言你所预期的调用已被执行,你还会检查它们是否以正确的顺序被执行并且没有额外的调用:

你使用 call 对象来构造列表以便与 mock_calls 进行比较:

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

然而,返回 mock 的调用的形参不会被记录,这意味着不可能追踪附带了重要形参的创建上级对象的嵌套调用:

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

设置返回值和属性

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

  1. >>> mock = Mock()
  2. >>> mock.return_value = 3
  3. >>> mock()
  4. 3

当然你也可以对 mock 上的方法做同样的操作:

  1. >>> mock = Mock()
  2. >>> mock.method.return_value = 3
  3. >>> mock.method()
  4. 3

返回值也可以在构造器中设置:

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

如果你需要在你的 mock 上设置一个属性,只需这样做:

  1. >>> mock = Mock()
  2. >>> mock.x = 3
  3. >>> mock.x
  4. 3

有时你会想要模拟更复杂的情况,例如这个例子 mock.connection.cursor().execute("SELECT 1")。 如果我们希望这个调用返回一个列表,那么我们还必须配置嵌套调用的结果。

我们可以像这样使用 call 在一个“链式调用”中构造调用集合以便随后方便地设置断言:

  1. >>> mock = Mock()
  2. >>> cursor = mock.connection.cursor.return_value
  3. >>> cursor.execute.return_value = ['foo']
  4. >>> mock.connection.cursor().execute("SELECT 1")
  5. ['foo']
  6. >>> expected = call.connection.cursor().execute("SELECT 1").call_list()
  7. >>> mock.mock_calls
  8. [call.connection.cursor(), call.connection.cursor().execute('SELECT 1')]
  9. >>> mock.mock_calls == expected
  10. True

.call_list() 的调用会将我们的调用对象转成一个代表链式调用的调用列表。

通过 mock 引发异常

一个很有用的属性是 side_effect。 如果你将该属性设为一个异常类或者实例那么当 mock 被调用时该异常将会被引发。

  1. >>> mock = Mock(side_effect=Exception('Boom!'))
  2. >>> mock()
  3. Traceback (most recent call last):
  4. ...
  5. Exception: Boom!

附带影响函数和可迭代对象

side_effect 也可以被设为一个函数或可迭代对象。 side_effect 作为可迭代对象的应用场景适用于你的 mock 将要被多次调用,并且你希望每次调用都返回不同的值的情况。 当你将 side_effect 设为一个可迭代对象时每次对 mock 的调用将返回可迭代对象的下一个值。

  1. >>> mock = MagicMock(side_effect=[4, 5, 6])
  2. >>> mock()
  3. 4
  4. >>> mock()
  5. 5
  6. >>> mock()
  7. 6

对于更高级的用例,例如根据 mock 调用时附带的参数动态改变返回值,side_effect 可以指定一个函数。 该函数将附带与 mock 相同的参数被调用。 该函数所返回的就是调用所返回的对象:

  1. >>> vals = {(1, 2): 1, (2, 3): 2}
  2. >>> def side_effect(*args):
  3. ... return vals[args]
  4. ...
  5. >>> mock = MagicMock(side_effect=side_effect)
  6. >>> mock(1, 2)
  7. 1
  8. >>> mock(2, 3)
  9. 2

模拟异步迭代器

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

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

模拟异步上下文管理器

从 Python 3.8 起,AsyncMockMagicMock 支持通过 __aenter____aexit__ 来模拟 异步上下文管理器。 在默认情况下,__aenter____aexit__ 将为返回异步函数的 AsyncMock 实例。

  1. >>> class AsyncContextManager:
  2. ... async def __aenter__(self):
  3. ... return self
  4. ... async def __aexit__(self, exc_type, exc, tb):
  5. ... pass
  6. ...
  7. >>> mock_instance = MagicMock(AsyncContextManager()) # AsyncMock also works here
  8. >>> async def main():
  9. ... async with mock_instance as result:
  10. ... pass
  11. ...
  12. >>> asyncio.run(main())
  13. >>> mock_instance.__aenter__.assert_awaited_once()
  14. >>> mock_instance.__aexit__.assert_awaited_once()

基于现有对象创建模拟对象

使用模拟操作的一个问题是它会将你的测试与你的 mock 实现相关联而不是与你的真实代码相关联。 假设你有一个实现了 some_method 的类。 在对另一个类的测试中,你提供了一个 同样 提供了 some_method 的模拟该对象的 mock 对象。 如果后来你重构了第一个类,使得它不再具有 some_method —— 那么你的测试将继续保持通过,尽管现在你的代码已经被破坏了!

Mock 允许你使用allows you to provide an object as a specification for the mock, using the spec 关键字参数来提供一个对象作为 mock 的规格说明。 在 mock 上访问不存在于你的规格说明对象中的方法 / 属性将立即引发一个属性错误。 如果你修改你的规格说明的实现,,那么使用了该类的测试将立即开始失败而不需要你在这些测试中实例化该类。

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

使用规格说明还可以启用对 mock 的调用的更聪明的匹配操作,无论是否有将某些形参作为位置或关键字参数传入:

  1. >>> def f(a, b, c): pass
  2. ...
  3. >>> mock = Mock(spec=f)
  4. >>> mock(1, 2, 3)
  5. <Mock name='mock()' id='140161580456576'>
  6. >>> mock.assert_called_with(a=1, b=2, c=3)

如果你想要让这些更聪明的匹配操作也适用于 mock 上的方法调用,你可以使用 auto-speccing

如果你想要更强形式的规格说明以防止设置任意属性并获取它们那么你可以使用 spec_set 来代替 spec

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

mock_open() 被用来为 open() 方法打补丁。 side_effect 可被用来在每次调用中返回一个新的 Mock 对象。 这可被用来返回存储在字典中的每个文件的不同内容:

  1. DEFAULT = "default"
  2. data_dict = {"file1": "data1",
  3. "file2": "data2"}
  4. def open_side_effect(name):
  5. return mock_open(read_data=data_dict.get(name, DEFAULT))()
  6. with patch("builtins.open", side_effect=open_side_effect):
  7. with open("file1") as file1:
  8. assert file1.read() == "data1"
  9. with open("file2") as file2:
  10. assert file2.read() == "data2"
  11. with open("file3") as file2:
  12. assert file2.read() == "default"

补丁装饰器

备注

在查找对象的名称空间中修补对象使用 patch() 。使用起来很简单,阅读 补丁的位置 来快速上手。

测试中的一个常见需求是为类属性或模块属性打补丁,例如修补内置对象或修补某个模块中的类来测试其是否被实例化。 模块和类都可算是全局对象,因此对它们打补丁的操作必须在测试完成之后被还原否则补丁将持续影响其他测试并导致难以诊断的问题。

为此 mock 提供了三个便捷的装饰器: patch(), patch.object()patch.dict()patch 接受单个字符串,其形式 package.module.Class.attribute 指明你要修补的属性。 它还可选择接受一个值用来替换指定的属性(或者类对象等等)。 ‘patch.object’ 接受一个对象和你想要修补的属性名称,并可选择接受要用作补丁的值。

patch.object:

  1. >>> original = SomeClass.attribute
  2. >>> @patch.object(SomeClass, 'attribute', sentinel.attribute)
  3. ... def test():
  4. ... assert SomeClass.attribute == sentinel.attribute
  5. ...
  6. >>> test()
  7. >>> assert SomeClass.attribute == original
  8. >>> @patch('package.module.attribute', sentinel.attribute)
  9. ... def test():
  10. ... from package.module import attribute
  11. ... assert attribute is sentinel.attribute
  12. ...
  13. >>> test()

如果你要给一个模块 (包括 builtins) 打补丁则可使用 patch() 来代替 patch.object():

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

如有必要模块名可以是“带点号”的,其形式如 package.module:

  1. >>> @patch('package.module.ClassName.attribute', sentinel.attribute)
  2. ... def test():
  3. ... from package.module import ClassName
  4. ... assert ClassName.attribute == sentinel.attribute
  5. ...
  6. >>> test()

一个良好的模式是实际地装饰测试方法本身:

  1. >>> class MyTest(unittest.TestCase):
  2. ... @patch.object(SomeClass, 'attribute', sentinel.attribute)
  3. ... def test_something(self):
  4. ... self.assertEqual(SomeClass.attribute, sentinel.attribute)
  5. ...
  6. >>> original = SomeClass.attribute
  7. >>> MyTest('test_something').test_something()
  8. >>> assert SomeClass.attribute == original

如果你想要通过 Mock 来打补丁,你可以只附带一个参数使用 patch() (或附带两个参数使用 patch.object())。 这将为你创建 mock 并传递给测试函数 / 方法:

  1. >>> class MyTest(unittest.TestCase):
  2. ... @patch.object(SomeClass, 'static_method')
  3. ... def test_something(self, mock_method):
  4. ... SomeClass.static_method()
  5. ... mock_method.assert_called_with()
  6. ...
  7. >>> MyTest('test_something').test_something()

你可以使用以下模式来堆叠多个补丁装饰器:

  1. >>> class MyTest(unittest.TestCase):
  2. ... @patch('package.module.ClassName1')
  3. ... @patch('package.module.ClassName2')
  4. ... def test_something(self, MockClass2, MockClass1):
  5. ... self.assertIs(package.module.ClassName1, MockClass1)
  6. ... self.assertIs(package.module.ClassName2, MockClass2)
  7. ...
  8. >>> MyTest('test_something').test_something()

当你嵌套 patch 装饰器时将以它们被应用的相同顺序(即 Python 应用装饰器的正常顺序)将 mock 传入被装饰的函数。 也就是说从下往上,因此在上面的示例中 test_module.ClassName2 的 mock 会被最先传入。

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

  1. >>> foo = {'key': 'value'}
  2. >>> original = foo.copy()
  3. >>> with patch.dict(foo, {'newkey': 'newvalue'}, clear=True):
  4. ... assert foo == {'newkey': 'newvalue'}
  5. ...
  6. >>> assert foo == original

patch, patch.objectpatch.dict 都可被用作上下文管理器。

在你使用 patch() 为你创建 mock 时,你可以使用 with 语句的 “as” 形式来获得对 mock 的引用:

  1. >>> class ProductionClass:
  2. ... def method(self):
  3. ... pass
  4. ...
  5. >>> with patch.object(ProductionClass, 'method') as mock_method:
  6. ... mock_method.return_value = None
  7. ... real = ProductionClass()
  8. ... real.method(1, 2, 3)
  9. ...
  10. >>> mock_method.assert_called_with(1, 2, 3)

作为替代 patch, patch.objectpatch.dict 可以被用作类装饰器。 当以此方式使用时其效果与将装饰器单独应用到每个以 “test” 打头的方法上相同。

更多示例

下面是一些针对更为高级应用场景的补充示例。

模拟链式调用

实际上一旦你理解了 return_value 属性那么使用 mock 模拟链式调用就会相当直观。 当一个 mock 首次被调用,或者当你在它被调用前获取其 return_value 时,将会创建一个新的 Mock

这意味着你可以通过检视 return_value mock 来了解从调用被模拟对象返回的对象是如何被使用的:

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

从这里开始只需一个步骤即可配置并创建有关链式调用的断言。 当然还有另一种选择是首先以更易于测试的方式来编写你的代码…

因此,如果我们有这样一些代码:

  1. >>> class Something:
  2. ... def __init__(self):
  3. ... self.backend = BackendProvider()
  4. ... def method(self):
  5. ... response = self.backend.get_endpoint('foobar').create_call('spam', 'eggs').start_call()
  6. ... # more code

假定 BackendProvider 已经过良好测试,我们要如何测试 method()? 特别地,我们希望测试代码段 # more code 是否以正确的方式使用了响应对象。

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

为了做到这一点我们创建一个 mock 实例作为我们的 mock 后端并为它创建一个 mock 响应对象。 要将该响应对象设为最后的 start_call 的返回值我们可以这样做:

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

我们可以通过更好一些的方式做到这一点,即使用 configure_mock() 方法直接为我们设置返回值:

  1. >>> something = Something()
  2. >>> mock_response = Mock(spec=open)
  3. >>> mock_backend = Mock()
  4. >>> config = {'get_endpoint.return_value.create_call.return_value.start_call.return_value': mock_response}
  5. >>> mock_backend.configure_mock(**config)

有了这些我们就能准备好给“mock 后端”打上猴子补丁并可以执行真正的调用:

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

使用 mock_calls 我们可以通过一个断言来检查链式调用。 一个链式调用就是在一行代码中连续执行多个调用,所以在 mock_calls 中将会有多个条目。 我们可以使用 call.call_list() 来为我们创建这个调用列表:

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

部分模拟

在某些测试中我希望模拟对 datetime.date.today() 的调用来返回一个已知的日期,但又不想阻止被测试的代码创建新的日期对象。 不幸的是 datetime.date 是用 C 语言编写的,因此我不能简单地给静态 datetime.date.today() 方法打上猴子补丁。

我找到了实现这一点的简单方式即通过一个 mock 来实际包装日期类,但通过对构造器的调用传递给真实的类(并返回真实的实例)。

这里使用 patch 装饰器 来模拟被测试模块中的 date 类。 然后将模拟 date 类的 side_effect 属性设为一个返回真实日期的 lambda 函数。 当模拟 date 类被调用时,将通过 side_effect 构造并返回一个真实日期。

  1. >>> from datetime import date
  2. >>> with patch('mymodule.date') as mock_date:
  3. ... mock_date.today.return_value = date(2010, 10, 8)
  4. ... mock_date.side_effect = lambda *args, **kw: date(*args, **kw)
  5. ...
  6. ... assert mymodule.date.today() == date(2010, 10, 8)
  7. ... 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” 方法模拟为生成器的示例类:

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

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

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

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

1

此外还有生成器表达式和更多的生成器 进阶用法,但在这里我们不去关心它们。 有关生成器及其强大功能的一个很好的介绍请参阅: 针对系统程序员的生成器妙招

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

如果你想要为多个测试方法准备好多个补丁那么最简单的方式就是将 patch 装饰器应用到每个方法上。 这在感觉上像上不必要的重复。 对此,你可以使用 patch() (包括基各种不同形式) 作为类装饰器。 这将把补丁应用于类上的所有测试方法。 测试方法是通过以 test 打头的名称来标识的:

  1. >>> @patch('mymodule.SomeClass')
  2. ... class MyTest(unittest.TestCase):
  3. ...
  4. ... def test_one(self, MockSomeClass):
  5. ... self.assertIs(mymodule.SomeClass, MockSomeClass)
  6. ...
  7. ... def test_two(self, MockSomeClass):
  8. ... self.assertIs(mymodule.SomeClass, MockSomeClass)
  9. ...
  10. ... def not_a_test(self):
  11. ... return 'something'
  12. ...
  13. >>> MyTest('test_one').test_one()
  14. >>> MyTest('test_two').test_two()
  15. >>> MyTest('test_two').not_a_test()
  16. 'something'

另一种管理补丁的方式是使用 补丁方法: start 和 stop。 它允许你将打补丁操作移至你的 setUptearDown 方法中。

  1. >>> class MyTest(unittest.TestCase):
  2. ... def setUp(self):
  3. ... self.patcher = patch('mymodule.foo')
  4. ... self.mock_foo = self.patcher.start()
  5. ...
  6. ... def test_foo(self):
  7. ... self.assertIs(mymodule.foo, self.mock_foo)
  8. ...
  9. ... def tearDown(self):
  10. ... self.patcher.stop()
  11. ...
  12. >>> MyTest('test_foo').run()

如果你要使用这个技巧则你必须通过调用 stop 来确保补丁被“恢复”。 这可能要比你想像的更麻烦,因为如果在 setUp 中引发了异常那么 tearDown 将不会被调用。 unittest.TestCase.addCleanup() 可以做到更方便:

  1. >>> class MyTest(unittest.TestCase):
  2. ... def setUp(self):
  3. ... patcher = patch('mymodule.foo')
  4. ... self.addCleanup(patcher.stop)
  5. ... self.mock_foo = patcher.start()
  6. ...
  7. ... def test_foo(self):
  8. ... self.assertIs(mymodule.foo, self.mock_foo)
  9. ...
  10. >>> MyTest('test_foo').run()

模拟未绑定方法

当前在编写测试时我需要修补一个 未绑定方法 (在类上而不是在实例上为方法打补丁)。 我需要将 self 作为第一个参数传入因为我想对哪些对象在调用这个特定方法进行断言。 问题是这里你不能用 mock 来打补丁,因为如果你用 mock 来替换一个未绑定方法那么当从实例中获取时它就不会成为一个已绑定方法,因而它不会获得传入的 self。 绕过此问题的办法是改用一个真正的函数来修补未绑定方法。 patch() 装饰器让使用 mock 来给方法打补丁变得如此简单以至于创建一个真正的函数成为一件麻烦事。

如果将 autospec=True 传给 patch 那么它就会用一个 真正的 函数对象来打补丁。 这个函数对象具有与它所替换的函数相同的签名,但会在内部将操作委托给一个 mock。 你仍然可以通过与以前完全相同的方式来自动创建你的 mock。 但是这将意味着一件事,就是如果你用它来修补一个类上的非绑定方法那么如果它是从一个实例中获取则被模拟的函数将被转为已绑定方法。 传给它的第一个参数将为 self,而这真是我想要的:

  1. >>> class Foo:
  2. ... def foo(self):
  3. ... pass
  4. ...
  5. >>> with patch.object(Foo, 'foo', autospec=True) as mock_foo:
  6. ... mock_foo.return_value = 'foo'
  7. ... foo = Foo()
  8. ... foo.foo()
  9. ...
  10. 'foo'
  11. >>> mock_foo.assert_called_once_with(foo)

如果我们不使用 autospec=True 那么这个未绑定方法会改为通过一个 Mock 补丁来修补,而不是附带 self 来调用。

通过 mock 检查多次调用

mock 有一个很好的 API 用于针对你的 mock 对象如何被使用来下断言。

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

如果你的 mock 只会被调用一次那么你可以使用 assert_called_once_with() 方法,该方法也会断言 call_count 的值为一。

  1. >>> mock.foo_bar.assert_called_once_with('baz', spam='eggs')
  2. >>> mock.foo_bar()
  3. >>> mock.foo_bar.assert_called_once_with('baz', spam='eggs')
  4. Traceback (most recent call last):
  5. ...
  6. AssertionError: Expected to be called once. Called 2 times.

assert_called_withassert_called_once_with 都是有关 最近 调用的断言。 如果你的 mock 将被多次调用,并且你想要针对 所有 这些调用下断言你可以使用 call_args_list:

  1. >>> mock = Mock(return_value=None)
  2. >>> mock(1, 2, 3)
  3. >>> mock(4, 5, 6)
  4. >>> mock()
  5. >>> mock.call_args_list
  6. [call(1, 2, 3), call(4, 5, 6), call()]

使用 call 辅助对象可以方便地针对这些调用下断言。 你可以创建一个预期调用的列表并将其与 call_args_list 比较。 这看起来与 call_args_list 的 repr 非常相似:

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

处理可变参数

另一种很少见,但可能给你带来麻烦的情况会在你的 mock 附带可变参数被调用的时候发生。 call_argscall_args_list 将保存对这些参数的 引用。 如果这些参数被受测试的代码所改变那么你将无法再针对当该 mock 被调用时附带的参数值下断言。

下面是一些演示此问题的示例代码。 设想在 ‘mymodule’ 中定义了下列函数:

  1. def frob(val):
  2. pass
  3. def grob(val):
  4. "First frob and then clear val"
  5. frob(val)
  6. val.clear()

当我们想要测试 grob 调用 frob 并附带了正确的参数时将可看到发生了什么:

  1. >>> with patch('mymodule.frob') as mock_frob:
  2. ... val = {6}
  3. ... mymodule.grob(val)
  4. ...
  5. >>> val
  6. set()
  7. >>> mock_frob.assert_called_with({6})
  8. Traceback (most recent call last):
  9. ...
  10. AssertionError: Expected: (({6},), {})
  11. Called with: ((set(),), {})

对于 mock 的一个可能性是复制你传入的参数。 如果你创建依赖于对象标识号相等性的断言那么这可能会在后面导致问题。

下面是一个使用 side_effect 功能的解决方案。 如果你为 mock 提供了 side_effect 函数那么 side_effect 将附带与该 mock 相同的参数被调用。 这样我们就有机会拷贝这些参数并将其保存起来用于之后执行断言。 在本例中我使用了 另一个 mock 来保存参数以便可以使用该 mock 的方法来执行断言。 在这里辅助函数再次为我设置好了这一切。

  1. >>> from copy import deepcopy
  2. >>> from unittest.mock import Mock, patch, DEFAULT
  3. >>> def copy_call_args(mock):
  4. ... new_mock = Mock()
  5. ... def side_effect(*args, **kwargs):
  6. ... args = deepcopy(args)
  7. ... kwargs = deepcopy(kwargs)
  8. ... new_mock(*args, **kwargs)
  9. ... return DEFAULT
  10. ... mock.side_effect = side_effect
  11. ... return new_mock
  12. ...
  13. >>> with patch('mymodule.frob') as mock_frob:
  14. ... new_mock = copy_call_args(mock_frob)
  15. ... val = {6}
  16. ... mymodule.grob(val)
  17. ...
  18. >>> new_mock.assert_called_with({6})
  19. >>> new_mock.call_args
  20. call({6})

调用 copy_call_args 时会传入将被调用的 mock。 它将返回一个新的 mock 供我们进行断言。 side_effect 函数会拷贝这些参数并附带该副本来调用我们的 new_mock

备注

如果你的 mock 只会被使用一次那么有更容易的方式可以在它们被调用时检查参数。 你可以简单地在 side_effect 函数中执行检查。

  1. >>> def side_effect(arg):
  2. ... assert arg == {6}
  3. ...
  4. >>> mock = Mock(side_effect=side_effect)
  5. >>> mock({6})
  6. >>> mock(set())
  7. Traceback (most recent call last):
  8. ...
  9. AssertionError

一个替代方式是创建一个 MockMagicMock 的子类来拷贝 (使用 copy.deepcopy()) 参数。 下面是一个示例实现:

  1. >>> from copy import deepcopy
  2. >>> class CopyingMock(MagicMock):
  3. ... def __call__(self, /, *args, **kwargs):
  4. ... args = deepcopy(args)
  5. ... kwargs = deepcopy(kwargs)
  6. ... return super().__call__(*args, **kwargs)
  7. ...
  8. >>> c = CopyingMock(return_value=None)
  9. >>> arg = set()
  10. >>> c(arg)
  11. >>> arg.add(1)
  12. >>> c.assert_called_with(set())
  13. >>> c.assert_called_with(arg)
  14. Traceback (most recent call last):
  15. ...
  16. AssertionError: Expected call: mock({1})
  17. Actual call: mock(set())
  18. >>> c.foo
  19. <CopyingMock name='mock.foo' id='...'>

当你子类化 MockMagicMock 时所有动态创建的属性以及 return_value 都将自动使用你的子类。 这意味着 CopyingMock 的所有子类也都将为 CopyingMock 类型。

嵌套补丁

使用 patch 作为上下文管理器很不错,但是如果你要执行多个补丁你将不断嵌套 with 语句使得代码越来越深地向右缩进:

  1. >>> class MyTest(unittest.TestCase):
  2. ...
  3. ... def test_foo(self):
  4. ... with patch('mymodule.Foo') as mock_foo:
  5. ... with patch('mymodule.Bar') as mock_bar:
  6. ... with patch('mymodule.Spam') as mock_spam:
  7. ... assert mymodule.Foo is mock_foo
  8. ... assert mymodule.Bar is mock_bar
  9. ... assert mymodule.Spam is mock_spam
  10. ...
  11. >>> original = mymodule.Foo
  12. >>> MyTest('test_foo').test_foo()
  13. >>> assert mymodule.Foo is original

使用 unittest cleanup 函数和 补丁方法: start 和 stop 我们可以达成同样的效果而无须嵌套缩进。 一个简单的辅助方法 create_patch 会为我们执行打补丁操作并返回所创建的 mock:

  1. >>> class MyTest(unittest.TestCase):
  2. ...
  3. ... def create_patch(self, name):
  4. ... patcher = patch(name)
  5. ... thing = patcher.start()
  6. ... self.addCleanup(patcher.stop)
  7. ... return thing
  8. ...
  9. ... def test_foo(self):
  10. ... mock_foo = self.create_patch('mymodule.Foo')
  11. ... mock_bar = self.create_patch('mymodule.Bar')
  12. ... mock_spam = self.create_patch('mymodule.Spam')
  13. ...
  14. ... assert mymodule.Foo is mock_foo
  15. ... assert mymodule.Bar is mock_bar
  16. ... assert mymodule.Spam is mock_spam
  17. ...
  18. >>> original = mymodule.Foo
  19. >>> MyTest('test_foo').run()
  20. >>> assert mymodule.Foo is original

使用 MagicMock 模拟字典

你可能会想要模拟一个字典或其他容器对象,记录所有对它的访问并让它的行为仍然像是一个字典。

要做到这点我们可以用 MagicMock,它的行为类似于字典,并会使用 side_effect 将字典访问委托给下层的在我们控制之下的一个真正的字典。

当我们的 MagicMock__getitem__()__setitem__() 方法被调用(即正常的字典访问操作)时 side_effect 将附带相应的键(对于 __setitem__ 还将附带值)被调用。 我们还可以控制返回的内容。

MagicMock 被使用之后我们可以使用 call_args_list 等属性来针对该字典是如何被使用的下断言。

  1. >>> my_dict = {'a': 1, 'b': 2, 'c': 3}
  2. >>> def getitem(name):
  3. ... return my_dict[name]
  4. ...
  5. >>> def setitem(name, val):
  6. ... my_dict[name] = val
  7. ...
  8. >>> mock = MagicMock()
  9. >>> mock.__getitem__.side_effect = getitem
  10. >>> mock.__setitem__.side_effect = setitem

备注

MagicMock 的一个可用替代是使用 Mock仅仅 提供你明确需要的魔术方法:

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

第三个 选项是使用 MagicMock 但传入 dict 作为 spec (或 spec_set) 参数以使得所创建的 MagicMock 只有字典魔术方法是可用的:

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

通过提供这些附带影响函数,mock 的行为将类似于普通字典但又会记录所有访问。 如果你尝试访问一个不存在的键它甚至会引发 KeyError

  1. >>> mock['a']
  2. 1
  3. >>> mock['c']
  4. 3
  5. >>> mock['d']
  6. Traceback (most recent call last):
  7. ...
  8. KeyError: 'd'
  9. >>> mock['b'] = 'fish'
  10. >>> mock['d'] = 'eggs'
  11. >>> mock['b']
  12. 'fish'
  13. >>> mock['d']
  14. 'eggs'

在它被使用之后你可以使用普通的 mock 方法和属性进行有关访问操作的断言:

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

模拟子类及其属性

你可能出于各种原因想要子类化 Mock。 其中一个可能的原因是为了添加辅助方法。 下面是一个笨兮兮的示例:

  1. >>> class MyMock(MagicMock):
  2. ... def has_been_called(self):
  3. ... return self.called
  4. ...
  5. >>> mymock = MyMock(return_value=None)
  6. >>> mymock
  7. <MyMock id='...'>
  8. >>> mymock.has_been_called()
  9. False
  10. >>> mymock()
  11. >>> mymock.has_been_called()
  12. True

The standard behaviour for Mock 实例的标准行为是属性和返回值 mock 具有与它们所访问的 mock 相同的类型。 这将确保 Mock 的属性均为 MocksMagicMock 的属性均为 MagicMocks 2。 因此如果你通过子类化来添加辅助方法那么它们也将在你的子类的实例的属性和返回值 mock 上可用。

  1. >>> mymock.foo
  2. <MyMock name='mock.foo' id='...'>
  3. >>> mymock.foo.has_been_called()
  4. False
  5. >>> mymock.foo()
  6. <MyMock name='mock.foo()' id='...'>
  7. >>> mymock.foo.has_been_called()
  8. True

有时这很不方便。 例如,一位用户 子类化了 mock 来创建一个 Twisted 适配器。 将它也应用于属性实际上会导致出错。

Mock (它的所有形式) 使用一个名为 _get_child_mock 的方法来创建这些用于属性和返回值的“子 mock”。 你可以通过重载此方法来防止你的子类被用于属性。 其签名被设为接受任意关键字参数 (**kwargs) 并且它们会被传递给 mock 构造器:

  1. >>> class Subclass(MagicMock):
  2. ... def _get_child_mock(self, /, **kwargs):
  3. ... return MagicMock(**kwargs)
  4. ...
  5. >>> mymock = Subclass()
  6. >>> mymock.foo
  7. <MagicMock name='mock.foo' id='...'>
  8. >>> assert isinstance(mymock, Subclass)
  9. >>> assert not isinstance(mymock.foo, Subclass)
  10. >>> assert not isinstance(mymock(), Subclass)

2

此规则的一个例外涉及不可调用 mock。 属性会使用可调用对象版本是因为如非如此则不可调用 mock 将无法拥有可调用的方法。

通过 patch.dict 模拟导入

有一种会令模拟变困难的情况是当你在函数内部有局部导入。 这更难模拟的原因是它们不是使用来自我们能打补丁的模拟命名空间中的对象。

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

除此之外还有一个办法可以使用 mock 来影响导入的结果。 导入操作会从 sys.modules 字典提取一个 对象。 请注意是提取一个 对象,它不是必须为模块。 首次导入一个模块将使一个模块对象被放入 sys.modules,因此通常当你执行导入时你将得到一个模块。 但是并非必然如此。

这意味着你可以使用 patch.dict()临时性地 将一个 mock 放入 sys.modules。 在补丁激活期间的任何导入操作都将得到该 mock。 当补丁完成时(被装饰的函数退出,with 语句代码块结束或者 patcher.stop() 被调用)则之前存在的任何东西都将被安全地恢复。

下面是一个模拟 ‘fooble’ 模拟的示例。

  1. >>> import sys
  2. >>> mock = Mock()
  3. >>> with patch.dict('sys.modules', {'fooble': mock}):
  4. ... import fooble
  5. ... fooble.blob()
  6. ...
  7. <Mock name='mock.blob()' id='...'>
  8. >>> assert 'fooble' not in sys.modules
  9. >>> mock.blob.assert_called_once_with()

你可以看到 import fooble 成功执行,而当退出时 sys.modules 中将不再有 ‘fooble’。

这同样适用于 from module import name 形式:

  1. >>> mock = Mock()
  2. >>> with patch.dict('sys.modules', {'fooble': mock}):
  3. ... from fooble import blob
  4. ... blob.blip()
  5. ...
  6. <Mock name='mock.blob.blip()' id='...'>
  7. >>> mock.blob.blip.assert_called_once_with()

稍微多做一点工作你还可以模拟包的导入:

  1. >>> mock = Mock()
  2. >>> modules = {'package': mock, 'package.module': mock.module}
  3. >>> with patch.dict('sys.modules', modules):
  4. ... from package.module import fooble
  5. ... fooble()
  6. ...
  7. <Mock name='mock.module.fooble()' id='...'>
  8. >>> mock.module.fooble.assert_called_once_with()

追踪调用顺序和不太冗长的调用断言

Mock 类允许你通过 method_calls 属性来追踪在你的 mock 对象上的方法调用的 顺序。 这并不允许你追踪单独 mock 对象之间的调用顺序,但是我们可以使用 mock_calls 来达到同样的效果。

因为 mock 会追踪 mock_calls 中对子 mock 的调用,并且访问 mock 的任意属性都会创建一个子 mock,所以我们可以基于父 mock 创建单独的子 mock。 随后对这些子 mock 的调用将按顺序被记录在父 mock 的 mock_calls 中:

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

我们可以随后通过与管理器 mock 上的 mock_calls 属性进行比较来进行有关这些调用,包括调用顺序的断言:

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

如果 patch 创建并准备好了你的 mock 那么你可以使用 attach_mock() 方法将它们附加到管理器 mock 上。 在附加之后所有调用都将被记录在管理器的 mock_calls 中。

  1. >>> manager = MagicMock()
  2. >>> with patch('mymodule.Class1') as MockClass1:
  3. ... with patch('mymodule.Class2') as MockClass2:
  4. ... manager.attach_mock(MockClass1, 'MockClass1')
  5. ... manager.attach_mock(MockClass2, 'MockClass2')
  6. ... MockClass1().foo()
  7. ... MockClass2().bar()
  8. <MagicMock name='mock.MockClass1().foo()' id='...'>
  9. <MagicMock name='mock.MockClass2().bar()' id='...'>
  10. >>> manager.mock_calls
  11. [call.MockClass1(),
  12. call.MockClass1().foo(),
  13. call.MockClass2(),
  14. call.MockClass2().bar()]

如果已经进行了许多调用,但是你只对它们的一个特定序列感兴趣则有一种替代方式是使用 assert_has_calls() 方法。 这需要一个调用的列表(使用 call 对象来构建)。 如果该调用序列在 mock_calls 中则断言将成功。

  1. >>> m = MagicMock()
  2. >>> m().foo().bar().baz()
  3. <MagicMock name='mock().foo().bar().baz()' id='...'>
  4. >>> m.one().two().three()
  5. <MagicMock name='mock.one().two().three()' id='...'>
  6. >>> calls = call.one().two().three().call_list()
  7. >>> m.assert_has_calls(calls)

即使链式调用 m.one().two().three() 不是对 mock 的唯一调用,该断言仍将成功。

有时可能会对一个 mock 进行多次调用,而你只对断言其中的 某些 调用感兴趣。 你甚至可能对顺序也不关心。 在这种情况下你可以将 any_order=True 传给 assert_has_calls:

  1. >>> m = MagicMock()
  2. >>> m(1), m.two(2, 3), m.seven(7), m.fifty('50')
  3. (...)
  4. >>> calls = [call.fifty('50'), call(1), call.seven(7)]
  5. >>> m.assert_has_calls(calls, any_order=True)

更复杂的参数匹配

使用与 ANY 一样的基本概念我们可以实现匹配器以便在用作 mock 的参数的对象上执行更复杂的断言。

假设我们准备将某个对象传给一个在默认情况下基于对象标识相等(这是 Python 中用户自定义类的默认行为)的 mock。 要使用 assert_called_with() 我们就将必须传入完全相同的对象。 如果我们只对该对象的某些属性感兴趣那么我们可以创建一个能为我们检查这些属性的匹配器。

在这个示例中你可以看到为何执行对 assert_called_with 的‘标准’调用并不足够:

  1. >>> class Foo:
  2. ... def __init__(self, a, b):
  3. ... self.a, self.b = a, b
  4. ...
  5. >>> mock = Mock(return_value=None)
  6. >>> mock(Foo(1, 2))
  7. >>> mock.assert_called_with(Foo(1, 2))
  8. Traceback (most recent call last):
  9. ...
  10. AssertionError: Expected: call(<__main__.Foo object at 0x...>)
  11. Actual call: call(<__main__.Foo object at 0x...>)

一个针对我们的 Foo 类的比较函数看上去会是这样的:

  1. >>> def compare(self, other):
  2. ... if not type(self) == type(other):
  3. ... return False
  4. ... if self.a != other.a:
  5. ... return False
  6. ... if self.b != other.b:
  7. ... return False
  8. ... return True
  9. ...

而一个可以使用这样的比较函数进行相等性比较运算的匹配器对象看上去会是这样的:

  1. >>> class Matcher:
  2. ... def __init__(self, compare, some_obj):
  3. ... self.compare = compare
  4. ... self.some_obj = some_obj
  5. ... def __eq__(self, other):
  6. ... return self.compare(self.some_obj, other)
  7. ...

将所有这些放在一起:

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

Matcher 是用我们的比较函数和我们想要比较的 Foo 对象来实例化的。 在 assert_called_with 中将会调用 Matcher 的相等性方法,它会将调用 mock 时附带的对象与我们创建我们的匹配器时附带的对象进行比较。 如果它们匹配则 assert_called_with 通过,而如果不匹配则会引发 AssertionError:

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

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

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