使用 Mock 的常见场景包括:
通过 Mock 可以替换对象的方法,检查系统的其它部分是否使用正确的参数调用了方法:
xxxxxxxxxx
from unittest.mock import MagicMock
class SomeClass:
def method(self):
pass
real = SomeClass()
real.method = MagicMock(name="method")
print(real.method(3, 4, 5, key="value"))
一旦 Mock 对象被使用(本例中的 real.method
),可以通过它的方法和属性断言它是被如何使用的。
一旦 Mock 对象被调用,它的 called
属性会被设置为 True。更为重要的是我们可以使用 assert_called_with()
和 assert_called_once_with()
方法检查是否使用正确的参数调用过 Mock 对象。
下面的例子测试了调用 ProductionClass().method
导致 something
方法被调用:
xxxxxxxxxx
from unittest.mock import MagicMock
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)
在上面的例子中,我们直接模拟了对象上的一个方法,检查它是否被正确的调用。另外一个常见情形是把 Mock 对象传递给一个方法,然后检查它是否被正确地使用。
下面的 ProductionClass
有一个 closer
方法,当使用一个对象调用它时,它会调用对象的 close 方法:
class ProductionClass:
def closer(self, something):
something.close()
因此为了测试它,我们需要传进去一个带 close
方法的对象,然后检查它是否被正确地调用:
x
from unittest.mock import Mock
class ProductionClass:
def closer(self, something):
something.close()
real = ProductionClass()
mock = Mock()
real.closer(mock)
mock.close.assert_called_with()
为了在 Mock 对象上提供 ‘close’ 方法,我们无需做任何事情。访问 close 时会自动创建它。因此,如果 ‘close’ 还没被调用过,在测试中访问它将会创建它,但是 assert_called_with()
方法会抛出失败异常。
Mock 的一种常见的使用情形是模拟在测试中被实例化的类。在模拟类时,使用 Mock 对象替代被模拟的类。通过调用被模拟的类来创建实例。即可以通过被模拟的类的返回值来访问 “mock 实例”。
在下面的例子中,函数 some_function
实例化 Foo
,并调用它的一个方法。patch()
使用 Mock 对象替换类 Foo
,Foo
实例是调用 Mock 对象的返回值,因此通过修改 Mock 对象的返回值来配置它:
# module.py
class Foo:
def method(self):
pass
xxxxxxxxxx
# mock_example.py
from unittest.mock import Mock, patch
import module
def some_function():
instance = module.Foo()
return instance.method()
with patch("module.Foo") as mock:
instance = mock.return_value
instance.method.return_value = "the result"
result = some_function()
assert result == "the result"
给 Mock 对象指定名字很有用。Mock 对象的名称会被展示在它的 repr 中,并且当 Mock 对象出现在测试失败消息中时,名称很有用。Mock 对象的名称也会传播给它的属性和方法:
>>> from unittest.mock import MagicMock
>>> mock = MagicMock(name="foo")
>>> mock
<MagicMock name='foo' id='4571021216'>
>>> mock.method
<MagicMock name='foo.method' id='4571264960'>
mock_calls
属性会记录所有对 Mock 对象的子属性以及子属性的子属性的调用:
x
from unittest.mock import MagicMock, call
mock = MagicMock(name="foo")
mock.method()
mock.attribute.method()
print(mock.mock_calls)
使用 call
对象可以构造用于与 mock_calls
比较的列表:
xxxxxxxxxx
expected = [call.method(), call.attribute.method()]
print(mock.mock_calls == expected)
然而,传递给返回 Mock 对象的调用的参数不会被记录:
xxxxxxxxxx
>>> from unittest.mock import Mock, call
>>> mock = Mock()
>>> mock.factory(important=True).delivery()
<Mock name='mock.factory().delivery()' id='4321992768'>
>>> call.factory(important=False).delivery() == mock.mock_calls[-1]
True
在 Mock 对象上设置返回值非常容易:
>>> from unittest.mock import Mock
>>> mock = Mock()
>>> mock.return_value = 3
>>> mock()
3
可以使用同样的方式给 Mock 对象上的方法设置返回值:
>>> from unittest.mock import Mock
>>> mock = Mock()
>>> mock.method.return_value = 3
>>> mock.method()
3
也可以在构造器中设置返回值:
xxxxxxxxxx
>>> from unittest.mock import Mock
>>> mock = Mock(return_value=3)
>>> mock()
3
如果需要在 Mock 对象上设置属性,可以这样做:
xxxxxxxxxx
>>> from unittest.mock import Mock
>>> mock = Mock()
>>> mock.x = 3
>>> mock.x
3
通过 Mock 可以模拟更复杂的情形,比如 mock.connection.cursor().execute("SELECT 1")
。如果我们想让这个调用返回一个列表,那么我们必须配置这个嵌套调用的返回结果。
可以使用 call
对象以“链式调用”的形式构造调用集:
from unittest.mock import Mock, call
mock = Mock()
cursor = mock.connection.cursor.return_value
cursor.execute.return_value = ["foo"]
print(mock.connection.cursor().execute("SELECT 1"))
expected = call.connection.cursor().execute("SELECT 1").call_list()
print(expected)
assert mock.mock_calls == expected
对 .call_list()
的调用会将 call 对象转换成代表“链式调用”的调用列表。
side_effect
是 Mock 的一个非常有用的属性。如果将其设置为一个异常类或实例,当 Mock 对象被调用时,该异常将被抛出:
>>> from unittest.mock import Mock
>>> mock = Mock(side_effect=Exception("Boom!"))
>>> mock()
Traceback (most recent call last):
...
Exception: Boom!
也可以将 side_effect
设置为函数或可迭代对象。对于 Mock 对象将要被调用多次,并且每次调用需要返回不同的值的情形,可以将 side_effect
指定为一个可迭代对象。当把 side_effect
设置为可迭代对象时,每次调用 Mock 对象将返回可迭代对象的下一个值:
>>> from unittest.mock import Mock
>>> mock = Mock(side_effect=[1, 2, 3, 4, 5])
>>> mock()
1
>>> mock()
2
>>> mock()
3
>>> mock()
4
>>> mock()
5
>>> mock()
Traceback (most recent call last):
...
StopIteration
对于更复杂的使用场景,比如根据调用 Mock 对象时传递的参数动态地更改返回值,可以将 side_effect
设置为函数。该函数将被使用与 Mock 对象相同的参数调用。该函数的返回值就是调用 Mock 对象的返回值:
from unittest.mock import MagicMock
vals = {(1, 2): 1, (2, 3): 2}
def side_effect(*args):
return vals[args]
mock = MagicMock(side_effect=side_effect)
print(mock(1, 2))
print(mock(2, 3))
使用 spec
参数可以把一个对象设置为 Mock 对象的规格。访问规格对象上不存在的属性或方法时,将立即抛出属性错误:
>>> from unittest.mock import Mock
>>> class SomeClass:
... def new_method(self):
... pass
...
>>> mock = Mock(spec=SomeClass)
>>> mock.old_method()
Traceback (most recent call last):
...
AttributeError: Mock object has no attribute 'old_method'
使用规格对象还可以更智能地匹配对 Mock 对象的调用,而不必关注参数的传递方式:
>>> from unittest.mock import Mock
>>> def f(a, b, c): pass
...
>>> mock = Mock(spec=f)
>>> mock(1, 2, 3)
<Mock name='mock()' id='4434241376'>
>>> mock.assert_called_with(a=1, b=2, c=3)
在测试中,一种常见的需求是模拟类属性或模块属性。如果模块和类是全局的,那么在测试之后,必须把它们复原。如果模拟对象在其它测试中持续存在,那么会导致难以诊断的问题。
为此,Mock 提供了三个便捷的装饰器:patch()
、patch.object()
、patch.dict()
。patch
接受一个字符串作为参数,该参数的形式是 package.module.Class.attribute
,用于指定要模拟的属性。它也可以接受一个可选的值作为参数,该值用于替换被模拟的属性。
patch.object
带三个参数:
xxxxxxxxxx
# module.py
class SomeClass:
attribute = object()
xxxxxxxxxx
# mock_example.py
from unittest.mock import sentinel, patch
from module import SomeClass
original_attribute = SomeClass.attribute
"module.SomeClass.attribute", sentinel.attribute) (
def test():
assert SomeClass.attribute == sentinel.attribute
test()
assert SomeClass.attribute == original_attribute
使用 patch()
可以模拟模块(包括 builtins):
from unittest.mock import sentinel, patch, MagicMock
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
:
from unittest.mock import sentinel, patch
'package.module.ClassName.attribute', sentinel.attribute) (
def test():
from package.module import ClassName
assert ClassName.attribute == sentinel.attribute
test()
比较优雅的使用方式是装饰测试方法本身:
import unittest
from unittest.mock import patch, sentinel
class SomeClass:
attribute = object()
class MyTest(unittest.TestCase):
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()
时,Mock 对象会被自动创建,并被传进测试函数 / 方法中:
import unittest
from unittest.mock import patch
class SomeClass:
def static_method():
pass
class MyTest(unittest.TestCase):
object(SomeClass, "static_method") .
def test_something(self, mock_method):
SomeClass.static_method()
mock_method.assert_called_with()
MyTest("test_something").test_something()
可以叠加多个 patch 装饰器:
import unittest
from unittest.mock import patch
import package.module
class MyTest(unittest.TestCase):
"package.module.ClassName1") (
"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 对象按照它们被应用的顺序传进函数。即按照从低向上的顺序将 Mock 对象传进函数。
patch.dict()
用于在一个作用域中设置字典的值,当测试结束时,字典会被恢复到原始状态:
from unittest.mock import patch
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 对象的引用:
from unittest.mock import patch
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” 开头的方法单独应用装饰器。
本文翻译自官方文档:https://docs.python.org/3/library/unittest.mock-examples.html。未完待续。
本文在 Python 3.8.2 测试通过