在单元测试中,你想把重点放在你要测试的代码上,避免处理你的代码可能使用的外部依赖。或者,你的程序可能有外部依赖,但在测试时却无法使用。在这些情况下,你可以在你的测试中使用mock。
本文将解释Python中mock库的基本功能,以及如何用一个简单的面向对象程序的例子在pytest中使用它。
什么是测试中的mock?
在测试中,mock是一个替代依赖关系的对象。当测试使用外部依赖的软件程序时,你可能想使用mock,理由如下。
- 你想把代码与依赖关系隔离开来,把注意力放在要测试的部分。
- 依赖关系是不可用的。
- 依赖关系是可用的,但在测试中使用它们的成本很高,包括金钱和时间。
在这种情况下,用可以模拟实际对象行为的对象来代替依赖是很方便的,而这正是mock所能提供的。
mock在Python中是如何工作的?
Python 有一个内置的 mock 库,叫做 unittest.mock。它有很多功能,但你只需要知道Mock对象的主要功能就可以开始了。
Mock 对象****是如何 工作的?
mock库有一个叫做Mock的类。当你创建一个Mock对象时,它就是一个Mock对象。如果你调用它,你会得到另一个Mock对象。如果你访问Mock对象的任何属性或方法,它会动态地创建它们并返回一个新的Mock对象。你可以配置它们以返回任何你喜欢的值。
打开一个python解释器,从unittest.mock中导入Mock。
$ python
Python 3.9.1 (v3.9.1:1e5d33e9b9, Dec 7 2020, 12:44:01)
[Clang 12.0.0 (clang-1200.0.32.27)] on darwin
输入 "help"、"copyright"、"credits "或 "license "获取更多信息。
>>> from unittest.mock import Mock
你可以从Mock类创建一个mock实例,你可以看到它是Mock类的一个对象。
>>> mock = Mock()
>>> type(mock)
<类'unittest.mock.Mock'>
当你调用Mock对象时,你会得到另一个Mock对象。
>>> mock()
<Mock name='mock()' id='4309251120′>。
你可以随即创建一个新的属性a。它默认返回一个新的Mock对象。
>>> mock.a
<Mock name='mock.a' id='4309251312′>。
>>> type(mock.a)
<class 'unittest.mock.Mock'>
你可以创建另一个叫做b的新属性,它也会返回一个新的Mock对象。
>>> mock.b
。
>>> type(mock.b)
<class 'unittest.mock.Mock'>
默认情况下,一个Mock对象在你调用它的时候会返回另一个Mock对象。
>>> type(mock.a())
<class 'unittest.mock.Mock'>
但是你可以使用return_value,这样该属性就像一个返回特定值的方法一样,例如,'A'。
>>> mock.a.return_value = 'A'。
>>> mock.a()
'A'
另外,你可以使用side_effect,这样,当mock被调用时,就会调用一个特定的函数。
>>> def func(n):
... 返回 n + 1
...
>>> mock.b.side_effect = func
>>> mock.b(1)
2
你可以在side_effect中指定一个错误或一个异常。例如,你可以配置一个mock来引发一个ZeroDivisionError。
>>> mock.c.side_effect = ZeroDivisionError
>>> mock.c()
回溯(最近的一次调用)。
文件"",第1行,在中
文件 "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/unittest/mock.py", line 1093, in __call__
返回 self._mock_call(*args, **kwargs)
文件 "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/unittest/mock.py", 行 1097, in _mock_call
return self._execute_mock_call(*args, **kwargs)
文件 "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/unittest/mock.py", 行 1152, in _execute_mock_call
提高效果
零除法错误
现在你可以创建一个mock,并让它以你喜欢的方式行事。
你如何 在测试中****使用Mock ?
Mock类提供了各种断言方法来验证mock对象的行为。下面的例子显示,mock模拟一直在跟踪调用(a()),你可以通过使用mock_calls来检查它。
>>> mock = Mock()
>>> mock.a()
<Mock name='mock.a()' id='4375069216′>。
>>> mock.mock_calls
[call.a()]
你可以通过使用assert_called()来验证一个特定的mock是否被调用,当断言成功时返回None。例如,如果你调用mock对象mock的方法a(),断言mock.a.assert_called()返回None(=pass)。
>>> mock.a()
<Mock name='mock.a()' id='4315854064′>。
>>> mock.a.assert_called()
如果a()没有被调用,断言会引发下面的AssertionError。
>>> mock = Mock()
>>> mock.a.assert_called()
回溯(最近一次调用)。
文件"",第1行,在中
文件 "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/unittest/mock.py", 876行, in assert_called
提出断言错误(msg)
AssertionError:预期'a'已经被调用。
有几个变种,如 assert_called_once(), assert_called_with(*args, **kwargs), assert_not_called(),等等,以检查更具体的断言条件。
你也可以检查调用的顺序。属性mock_calls返回所有的调用,assert_has_calls()检查指定的(预期的)调用是否与实际调用相符。
>>> from unittest.mock import Mock, call
>>> mock = Mock()
>>> mock.a()
<Mock name='mock.a()' id='4346518544′>。
>>> mock.b()
<Mock name='mock.b()' id='4346663312′> >> mock.b()
>>> mock.a()
<Mock name='mock.a()' id='4346518544′> >> mock.a()
>>> mock.mock_calls
[call.a(), call.b(), call.a()] 。
>> mock.assert_has_calls([call.a(), call.b(), call.a()])
如果实际的调用顺序与预期的调用顺序不同,将引发一个断言错误。
>>> mock.assert_has_calls([call.a(), call.a(), call.b()] )
回溯(最近的一次调用)。
文件"",第1行,在中
文件 "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/unittest/mock.py", 944行, in assert_has_calls
提出断言错误(AssertionError)
AssertionError。没有找到调用。
预期的是。[call.a(), call.a(), call.b()] 。
实际:[call.a(), call.b(), call.a()] 。
在Python中****使用Mock进行单元测试的例子
在本节中,我将使用Martin Fowler写的《Mocks Aren't Stubs》一文中的一个例子。
我们有两个类,仓库和订单。仓库对象持有各种产品的库存。当我们得到一个Order对象时,我们从一个Warehouse对象中填充它。如果仓库对象有足够的产品,库存就会减少,而订单就会被填满。如果没有足够的产品,订单就不会被填写。
这里,让我们假设我们已经知道了仓库类的规范。所以,我们可以根据规范来实现订单类,如下图所示。
order.py
class Order:
def __init__(self, product, quantity):
self._product = product
self._quantity = quantity
self._filled = False
def fill(self, warehouse):
has_inventry = warehouse.has_inventory(
self._product,
self._quantity
)
如果 has_inventry:
warehouse.remove(self._product, self._quantity)
self._filled = True
def is_filled(self):
return self._filled
订单类的初始化器需要两个参数,产品和数量,它们分别被设置为属性self._product和self._quantity。
该类有一个名为fill()的实例方法,它接收一个仓库对象。根据(假想的)规范,我们需要用参数self._product和self._quantity调用其方法has_inventory()来检查库存。
has_inventory()方法返回True或False,这取决于库存状态。如果它有足够数量的产品,它就会返回True,所以fill()方法可以调用remove()来更新库存。然后,我们可以将属性self._filled设置为True,表示订单已经被填充。
如果库存中没有足够数量的产品,has_inventory()返回False,所以self._filled的值保持为False,表明订单没有被填充。
实例方法is_filled()返回self._filled属性的当前值。
现在让我们来看看如何为这段代码运行单元测试。我们将对仓库对象进行模拟,因为我们只关注订单类。
如何 在pytest中****使用Mock
如下图所示,我们可以写第一个测试用例来检查当仓库有足够的产品时,订单是否被填充。
test_order.py
from unittest.mock import Mock
from order import Order
def test_order_is_filled()。
# 设置 - 数据
order = Order('Talisker', 50)
warehouse = Mock()
# 设置 - 预期
warehouse.has_inventory.return_value = True
warehouse.remove.return_value = None
# 练习
order.fill( warehouse)
# 验证
assert order.is_filled()
首先,我们需要从 unittest.mock 中导入 Mock 类。我们还需要导入订单类来进行测试。
测试的第一部分是设置测试数据。我们可以创建一个数量为50的Talisker订单。然后,我们需要一个Warehoue对象。由于我们对测试仓库类不感兴趣(或者我们可能无法在测试环境中访问仓库类),我们通过使用Mock来模拟一个仓库对象的行为,模拟一个仓库对象。这意味着 warehouse 不是一个实际的 Warehouse 对象,但其行为类似于 Warehoue 对象,而且我们可以控制它的行为方式。
在第二部分,我们设置期望值。正如我们之前看到的,我们可以在Mock对象中动态地创建任何方法,所以我们可以创建has_inventory()方法并将其设置为返回True,模拟库存有足够的Talisker。然后我们创建remove()方法,该方法返回None。
在第三部分,订单对象以仓库对象为参数调用 fill() 方法。订单对象不知道 warehouse 是一个 Mock 对象,所以它只是简单地运行这个方法,就像它是一个真正的 warehouse 对象一样。
最后,我们检查订单是否已经被填充。在这种情况下,is_filled() 应该返回 True,因为 has_inventory() 返回 True。
你可以通过运行pytest来检查测试是否通过。
$ pytest test_order.py -v
==================== 测试会话开始 ====================
平台 darwin - Python 3.9.1, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 - /Users/mikio/pytest4/venv/bin/python3
cachedir: .pytest_cache
rootdir:/Users/mikio/pytest4
收集了1个项目
test_order.py::test_order_is_filled PASSED [100%]
===================== 0.04s内通过了1项 =====================
我们还可以添加另一个测试案例,即仓库没有足够的存货。
test_order.py
from unittest.mock import Mock
from order import Order
def test_order_is_filled()。
# 设置 - 数据
order = Order('Talisker', 50)
warehouse = Mock()
# 设置 - 预期
warehouse.has_inventory.return_value = True
warehouse.remove.return_value = None
# 练习
order.fill( warehouse)
# 验证
assert order.is_filled()
def test_order_is_not_filled()。
# 设置 - 数据
order = Order('Talisker', 50)
warehouse = Mock()
# 设置 - 预期
warehouse.has_inventory.return_value = False
# 练习
order.fill( warehouse)
# 验证
assert not order.is_filled()
在第二个测试案例中,模拟仓库对象的has_inventory()方法返回False,这模拟了库存中没有足够的Talisker。所以,订单没有被填充,is_filled()方法应该返回False。你可以检查第二个测试是否也通过了。
$ pytest test test_order.py -v
==================== 测试会话开始 ====================
平台 darwin - Python 3.9.1, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 - /Users/mikio/pytest4/venv/bin/python3
cachedir: .pytest_cache
rootdir:/Users/mikio/pytest4
收集了2个项目
test_order.py::test_order_is_filled PASSED [ 50%]
test_order.py::test_order_is_not_filled PASSED [100%] 。
===================== 2在0.03s内通过 =====================
如果上面的例子乍看之下令人困惑,你可以试着改变返回值,看看会发生什么。例如,如果你在第二个测试中把has_inventory的返回值改为True,测试将失败。
$ pytest test_order.py -v
==================== 测试会话开始 ====================
平台 darwin - Python 3.9.1, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 - /Users/mikio/pytest4/venv/bin/python3
cachedir: .pytest_cache
rootdir:/Users/mikio/pytest4
收集了2个项目
test_order.py::test_order_is_filled PASSED [ 50%]
test_order.py::test_order_is_not_filled 失败 [100%]
========================= FAILURES ==========================
_________________ test_order_is_not_filled __________________
def test_order_is_not_filled()。
# 设置 - 数据
order = Order('Talisker', 50)
warehouse = Mock()
# 设置 - 预期
warehouse.has_inventory.return_value = True
# 练习
order.fill( warehouse)
# 验证
> assert not order.is_filled()
E 断言不是真
E + where True = <bound method Order.is_filled of <order.Order object at 0x10b2481c0>>()
E + where <bound method Order.is_filled of <order.Order object at 0x10b2481c0>> = <order.Order object at 0x10b2481c0>.is_filled
test_order.py:33: AssertionError
================== 短的测试摘要信息 ==================
FAILED test_order.py::test_order_is_not_filled - assert no...
================ 1次失败,1次通过,时间为0.05s ================
你可以看到is_filled()方法返回True。这是因为has_inventory返回True,意味着库存有足够的Talisker。希望你能看到,你可以完全控制依赖关系(= warehouse)的行为,因为我们使用的是Mock而不是真正的Warehouse对象。
如何 在pytest中 使用Mock断言方法调用
正如我们前面看到的,Mock类有各种关于方法调用的断言方法。因此,我们可以检查订单对象内部调用了哪些方法。
我们可以在每个测试的最后添加两个额外的断言语句,如下所示。
test_order.py
from unittest.mock import Mock, call
from order import Order
def test_order_is_filled()。
# 设置 - 数据
order = Order('Talisker', 50)
warehouse = Mock()
# 设置 - 预期
warehouse.has_inventory.return_value = True
warehouse.remove.return_value = None
# 练习
order.fill( warehouse)
# 验证
assert order.is_filled()
warehouse.has_inventory.assert_called_with('Talisker', 50)
warehouse.assert_has_calls(
[
call.has_inventory('Talisker', 50),
call.remove('Talisker', 50)
]
)
def test_order_is_not_filled()。
# 设置 - 数据
order = Order('Talisker', 50)
warehouse = Mock()
# 设置 - 预期
warehouse.has_inventory.return_value = False
# 练习
order.fill( warehouse)
# 验证
assert not order.is_filled()
warehouse.has_inventory.assert_called_with('Talisker', 50)
warehouse.remove.assert_not_called()
在第一个测试中,我们可以添加这两个断言。
warehouse.has_inventory.assert_called_with('Talisker', 50)
warehouse.assert_has_calls(
[
call.has_inventory('Talisker', 50)。
call.remove('Talisker', 50)
]
)
第一个 assert_called_with() 方法检查仓库对象是否调用了 has_inventory() 方法,参数为 'Talisker', 50。
第二个 assert_has_calls() 方法验证了仓库对象已经按照这个顺序调用了两个方法(has_inventory('Talisker', 50) 和 remove('Talisker', 50))。
我们也可以在第二个测试中添加类似的断言。
warehouse.has_inventory.assert_called_with('Talisker', 50)
warehouse.remove.assert_not_called()
第一个 assert_called_with() 与第一个测试相同,但第二个 assert_not_called() 检查仓库对象是否没有调用 remove() 方法。有这个断言很重要,因为我们不能仅仅通过查看is_filled()方法的输出来检查它。
我们可以确认测试仍然通过。
$ pytest test_order.py -v
==================== 测试会话开始 ====================
平台 darwin - Python 3.9.1, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 - /Users/mikio/pytest4/venv/bin/python3
cachedir: .pytest_cache
rootdir:/Users/mikio/pytest4
收集了2个项目
test_order.py::test_order_is_filled PASSED [ 50%]
test_order.py::test_order_is_not_filled PASSED [100%] 。
===================== 2个项目在0.05s内通过 =====================
考虑因素
嘲讽是测试中一个强大的工具,但也有一些注意事项。
首先,Mock需要维护。由于我们根据规范自己创建了mock,对依赖关系的任何改变也需要在mock中实现。否则,所有的单元测试都可能通过,但整个软件程序将无法运行。如果你严格遵循开放-封闭的原则,这可能不是一个大问题,但这取决于你如何开发软件。
这就引出了第二点。Mocks在某些软件设计风格中比其他风格更有效。Martin Fowler在上面提到的《Mocks Aren't Stubs》一文中强调了使用mock的测试人员(mockist)和更多古典风格的测试人员(classicists)之间的区别,他说其中一个区别是mockist更喜欢 "由外而内的方法"。
一旦你有了第一个测试的运行,mock上的期望值就为下一步提供了规范,也为测试提供了起点。你把每一个期望变成一个合作者的测试,并重复这个过程,一次一个SUT的方式进入系统。这种风格也被称为 "由外而内",这是一个非常具有描述性的名字。它在分层系统中运行良好。你首先从UI编程开始,使用下面的模拟层。然后,你为下层编写测试,逐渐地一层一层地完成系统。这是一个非常结构化和受控的方法,许多人认为这对指导OO和TDD的新人是有帮助的。
在上面的例子中,test_order.py中的期望将成为仓库类的规范,这样我们就可以根据这个规范来创建测试,以此类推。你可以阅读这篇文章以获得更多关于单元测试、嘲弄和测试驱动开发的一般见解。
摘要
在这篇文章中,我通过观察 unittest.mock 库中 Mock 的基本功能,解释了模拟在 Python 中是如何工作的。它是一个方便的工具,可以替代外部依赖并设置测试。然后我们通过使用一个面向对象的程序的例子,看了如何将它与pytest一起使用。
我希望这篇文章能帮助你理解mock的工作原理,以及如何将其与pytest一起使用以更有效地运行测试。
The postHow to Use Mock in PyTest?first appeared onFinxter.