使用 Mock 的常见场景包括:
通过 Mock 可以替换对象的方法,检查系统的其它部分是否使用正确的参数调用了方法:
xxxxxxxxxxfrom unittest.mock import MagicMockclass SomeClass: def method(self): passreal = 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 方法被调用:
xxxxxxxxxxfrom unittest.mock import MagicMockclass ProductionClass: def method(self): self.something(1, 2, 3) def something(self, a, b, c): passreal = 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 Mockclass 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.pyclass Foo: def method(self): passxxxxxxxxxx# mock_example.pyfrom unittest.mock import Mock, patchimport moduledef 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, callmock = MagicMock(name="foo")mock.method()mock.attribute.method()print(mock.mock_calls)使用 call 对象可以构造用于与 mock_calls 比较的列表:
xxxxxxxxxxexpected = [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.x3通过 Mock 可以模拟更复杂的情形,比如 mock.connection.cursor().execute("SELECT 1")。如果我们想让这个调用返回一个列表,那么我们必须配置这个嵌套调用的返回结果。
可以使用 call 对象以“链式调用”的形式构造调用集:
from unittest.mock import Mock, callmock = Mock()cursor = mock.connection.cursor.return_valuecursor.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 MagicMockvals = {(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.pyclass SomeClass: attribute = object()xxxxxxxxxx# mock_example.pyfrom unittest.mock import sentinel, patchfrom module import SomeClassoriginal_attribute = SomeClass.attribute("module.SomeClass.attribute", sentinel.attribute)def test(): assert SomeClass.attribute == sentinel.attributetest()assert SomeClass.attribute == original_attribute使用 patch() 可以模拟模块(包括 builtins):
from unittest.mock import sentinel, patch, MagicMockmock = 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.attributetest()比较优雅的使用方式是装饰测试方法本身:
import unittestfrom unittest.mock import patch, sentinelclass SomeClass: attribute = object()class MyTest(unittest.TestCase): .object(SomeClass, 'attribute', sentinel.attribute) def test_something(self): self.assertEqual(SomeClass.attribute, sentinel.attribute)original = SomeClass.attributeMyTest("test_something").test_something()assert SomeClass.attribute == original当使用只带一个参数的 patch() 或者带两个参数的 patch.object() 时,Mock 对象会被自动创建,并被传进测试函数 / 方法中:
import unittestfrom unittest.mock import patchclass SomeClass: def static_method(): passclass 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 unittestfrom unittest.mock import patchimport package.moduleclass 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 patchfoo = {'key': 'value'}original = foo.copy()with patch.dict(foo, {'newkey': 'newvalue'}, clear=True): assert foo == {'newkey': 'newvalue'}assert foo == originalpatch()、patch.object()、patch.dict() 支持上下文管理协议。
当使用 patch() 创建 Mock 对象时,可以通过 with 语句的 as 形式获取 Mock 对象的引用:
from unittest.mock import patchclass ProductionClass: def method(self): passwith 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 测试通过