Categories 技术、软件和代码
使用 Python 的补丁装饰器进行单元测试
经过 拉尔夫十一月 5, 2020发表评论 on Unit Testing with Python’s Patch Decorator
如果您不熟悉 Python 中的模拟,请使用 unittest.mock
图书馆可能有点吓人。 在这篇文章中,我将向您介绍一些常用的使用方法 patch
装饰师。 修补程序是高度可配置的,并且有几个不同的选项来完成相同的结果。 选择一种方法而不是另一种方法可能是一项任务。 我将通过演示我常用的修补方法来为您简化这一点。 了解这些技术将涵盖您在日常 Python 编码中将面临的大部分模拟需求。 让我们从一个示例类开始测试.
在我们开始之前
我假设你已经很好地理解了单元测试,所以我不会在这里介绍单元测试的基础知识。 如果你想跟随,设置 python 和 pip install mock
. 本文使用的源代码可以下载 这里.
我们在测试什么?
from src.api import school as school_api
class Student(object):
def __init__(self, student_name, school_name):
self.student_name = student_name
self.school_name = school_name
def get_details(self):
school = school_api.get_school(self.school_name)
student_details = {'student_name': self.student_name, 'school': school}
return student_details
上面的课程非常简单。 它是 Student
将学生姓名和学校名称作为构造函数参数的类。 这 Student
有一个 get_details
调用 API 以获取有关给定学校的详细信息的方法。 我们将测试这个方法并使用 patch
模拟 API 调用.
测试用例设置
让我们创建一个简单的 TestCase
并为 API 调用创建一个模拟函数。 School API 需要一个 school_name
并返回一个 dict
包含给定的 school_name
, 随着 teacher_count
和 student_count
. 我们将创建一个与 API 具有相同方法签名的函数,并返回一些模拟数据作为预期结果.
import mock
import unittest
from unittest import TestCase
from src.student import Student
def mock_get_school(school_name):
return {"school_name": school_name, "student_count": 100, "teacher_count": 8}
class StudentTest(TestCase):
def setUp(self):
self.student = Student("Tester", "Test High")
mock_get_school()
是我们的补丁程序在遇到 school_api.get_school()
测试过程中的方法。 这样,我们就可以测试 Student
类而不尝试进行远程调用。 在我们修补之前,让我们设置我们的测试方法.
测试方法
def test_get_details(self):
student_details = self.student.get_details()
assert student_details["student_name"] == self.student.student_name
assert student_details["school"]["school_name"] == self.student.school_name
assert student_details["school"]["student_count"] == 100
assert student_details["school"]["teacher_count"] == 8
这 test_get_details
方法将使用 student
我们在测试初始化期间设置的。 调用后 student.get_details()
我们将运行一些断言以确保我们得到我们期望的结果。 如果一切顺利,被测类将被注入我们的模拟并返回预期的模拟数据.
Python补丁装饰器
方法 #1 – 简单替换
第一种方法很简单,您可能会有很多用例。 我们将配置装饰器以针对学校 api 并为其提供一个方法来替换真正的 API 调用。 第一个论据 patch
将是 api 方法的查找路径。 这将与您的测试类中使用的导入路径相同。 第二个参数将是我们在上面创建的模拟方法。 该方法只是替换目标对象,没有 mock
实例曾经被创建.
@mock.patch("src.api.school.get_school", mock_get_school)
这是最终测试方法的样子.
@mock.patch("src.api.school.get_school", mock_get_school)
def test_get_details(self):
student_details = self.student.get_details()
assert student_details["student_name"] == self.student.student_name
assert student_details["school"]["school_name"] == self.student.school_name
assert student_details["school"]["student_count"] == 100
assert student_details["school"]["teacher_count"] == 8
这很简单,老实说,一个简单的替换通常会满足您的需求。 只要在测试期间调用 API,API 就会被模拟函数替换。 现在您可以在不需要远程服务的情况下测试 Student 类.
优点
- 简单的
- 装饰器之外没有额外的代码
缺点
- 您的模拟函数不能是 TestCase 上的方法(除非它是静态的)
- 您的函数不能(轻松)对后续调用做出不同的响应
- 没有创建模拟
- 没有对象可以通过后续断言进行检查(它被调用了多少次)
方法 #2 – 生成的模拟参数
在第二种方法中,我们将配置装饰器,以便它为您提供一些您在第一种方法中看不到的好处。 这一次,我们只将目标路径传递给 patch
装饰师。 当我们这样做时,装饰器将生成一个 MagicMock
对象并将其作为参数传递给我们的 TestCase
方法。 顺便说一句,你会得到一个 AsyncMock
如果模拟一个异步类.
@mock.patch("src.api.school.get_school")
def test_get_details_2(self, mock_school_api):
mock_school_api.side_effect = mock_get_school
student_details = self.student.get_details()
assert student_details["school"]["school_name"] == self.student.school_name
assert student_details["school"]["student_count"] == 100
assert student_details["school"]["teacher_count"] == 8
mock_school_api.assert_called_once()
这里发生的是 patch
创建了一个模拟对象并将其作为参数传递给我们的测试方法。 通过对 mock 的引用,我们可以配置它的行为并在调用后进行检查。 参数可以是你给它的任何名称。 我正在使用 mock_school_api
, 这将是 MagicMock 的一个实例。 它有一个 side_effect
我配置为在调用时调用我们的模拟函数的属性。 因为我们有模拟实例,我们可以在它上面运行断言,或者将它配置为动态响应后续调用。 此外,side_effect 可以是外部函数或定义在 TestCase
类本身.
优点
- 更大的灵活性
- 允许动态配置
- mock 的显式配置
- 您对后续断言的模拟有参考
缺点
- 额外的复杂性
- 更多代码行
- 每个模拟都会添加到您的参数列表中
- 重复测试场景的潜在配置重复
方法#3——外部配置
方法 2 的最大缺点是为您的测试方法和配置步骤(设置 side_effect、return_value 等)提供了额外的参数,这在您的测试中发生。 但这真的有那么糟糕吗? 如果你只有几个测试,不,这还不错。 当您有许多快乐/不快乐/异常路径时,参数和配置的重复将很明显。 但是,我们可以通过外部化配置来避免重复配置。 为此,将修补程序传递给装饰器的配置(**kwargs)作为最后一个参数.
PATCH_CONFIG = {'side_effect': mock_get_school}
@mock.patch("src.api.school.get_school", **PATCH_CONFIG)
如果您有许多具有相同模拟配置的测试,这是可靠的方法.
优点
- 和以前一样,没有重复配置
缺点
- 真的没有
- 您仍然可以将参数传递给您的测试方法,但是如果您在一种方法中有很多模拟,那只会很糟糕(不要那样做)
方法 #4 – 显式模拟对象创建
第四种方法在某种程度上是一种混合方法。 在这里,我们可以创建一个 MagicMock
明确地将其作为第二个参数传递给 patch
. 这样做,它允许我们引用模拟以进行后续检查(无需将其作为参数传递给测试方法)。 我们还可以在一个地方配置模拟,并在我们重复测试时保持我们的测试方法没有重复配置。 这是整个测试类.
import mock
import unittest
from unittest import TestCase
from src.student import Student
def mock_get_school(school_name):
return {"school_name": school_name, "student_count": 100, "teacher_count": 8}
class StudentTest(TestCase):
mock_school_api = mock.MagicMock(side_effect=mock_get_school)
def setUp(self):
self.student = Student("Tester", "Test High")
StudentTest.mock_school_api.reset_mock()
@mock.patch("src.api.school.get_school", mock_school_api)
def test_get_details_4(self):
student_details = self.student.get_details()
assert student_details["school"]["school_name"] == self.student.school_name
assert student_details["school"]["student_count"] == 100
assert student_details["school"]["teacher_count"] == 8
StudentTest.mock_school_api.assert_called_once()
if __name__ == "__main__":
unittest.main()
在这里,我们明确地创建 mock_school_api
作为一个 MagicMock
. 它是类的静态成员,因此可以在装饰器上使用。 注意 setUp
方法。 我们称之为 reset_mock()
在我们的模拟实例上每次测试运行。 这非常重要,因此每个测试都有一个新的模拟开始。 我们明确地创建了模拟,所以我们必须维护它。 修补程序角色只是注入器。 它将目标对象替换为我们创建的模拟对象,并且不再对其进行任何操作.
优点
- 您具有传递模拟实例的相同灵活性,而无需在每个测试方法上使用额外的参数
- 您可以在一处配置模拟,减少重复
- 您有一个用于后续断言的模拟实例
缺点
- 您必须自己管理模拟实例
- 如果您没有正确重置,模拟会在每个测试中保留状态
作为一个 选择, 您可以在不使用装饰器的情况下显式创建模拟。 修补程序可以直接在目标路径上作为函数调用。 您仍然必须通过启动和停止修补程序(setUp 和 tearDown).
patcher = patch('src.api.school.get_school')
mock_school_api = patcher.start()
# invocations
# assertions
patcher.stop()
类装饰器
patch
也可以用作类级别的装饰器。 使用类装饰器,你将不能传入你正在装饰的类中存在的模拟函数。 如果您有几种相同类型的测试方法并且您需要为每种方法提供完全相同的模拟,通常会使用此方法。 修补程序会将所有以“test”开头的方法识别为需要模拟参数的方法。 将为每个创建一个模拟 patch
你在课堂上并传递给每个合格的测试方法.
概括
这篇文章重点介绍了 Python 中的一些常用模拟方法。 这 unittest.mock
库是高度可配置的,并且允许您以比我在这里概述的方式更多的方式来模拟方法和对象。 当然,有这么多的选择,这些功能最初似乎是压倒性的。 话虽如此,一旦您掌握了基础知识,我鼓励您探索文档。 在那里,您将找到所有可用模拟技术和配置的完整列表。 有了你在这里学到的东西,我相信你正朝着正确的方向前进。 谢谢阅读.