如何在PyTest中使用Mock?

759 阅读6分钟

在单元测试中,你想把重点放在你要测试的代码上,避免处理你的代码可能使用的外部依赖。或者,你的程序可能有外部依赖,但在测试时却无法使用。在这些情况下,你可以在你的测试中使用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.