Dane Hillard
pytest 是一个流行的 Python 测试框架,它简化了编写和执行测试的过程。要开始使用 pytest,请在虚拟环境中通过 pip 安装它。与 Python 自带的 unittest 相比,pytest 具有多项优势,例如更少的样板代码、更易读的输出以及丰富的插件生态系统。
完成本教程后,你将理解以下内容:
- 使用 pytest 需要在虚拟环境中通过 pip 安装,以启用
pytest命令。 - 与 unittest 相比,pytest 所需代码更少、可读性更强,并提供更多功能。
- pytest 通过 fixtures(夹具) 高效管理测试依赖和状态,显式声明依赖关系。
- pytest 的 参数化(parametrization) 功能可避免冗余测试代码,从单个测试函数生成多个测试场景。
- pytest 的 断言内省(assertion introspection) 在测试失败时提供详细的错误信息。
如何安装 pytest
要跟随本教程中的示例操作,你需要先安装 pytest。和大多数 Python 包一样,pytest 可在 PyPI 上获取。你可以使用 pip 在虚拟环境中安装它:
# Windows / Linux / macOS
$ python -m venv venv
$ source venv/bin/activate # Linux/macOS
# 或
$ venv\Scripts\activate # Windows
(venv) $ python -m pip install pytest
安装完成后,pytest 命令即可在当前环境中使用。
pytest 为何如此实用?
如果你之前为 Python 代码编写过单元测试,可能用过 Python 内置的 unittest 模块。unittest 提供了构建测试套件的坚实基础,但也存在一些不足。
许多第三方测试框架试图解决 unittest 的问题,而 pytest 已成为其中最受欢迎的选择之一。pytest 是一个功能丰富、基于插件生态系统的测试框架。
如果你尚未使用过 pytest,那你将大有收获!它的设计理念和功能将使你的测试体验更加高效和愉快。使用 pytest,常见任务所需代码更少,高级任务则可通过各种节省时间的命令和插件实现。它甚至能直接运行你现有的测试,包括那些用 unittest 编写的测试。
与大多数框架一样,某些在初学 pytest 时看似合理的开发模式,随着测试套件规模扩大可能会带来问题。本教程将帮助你了解 pytest 提供的工具,即使在大规模测试中也能保持高效和有效。
更少的样板代码
大多数功能测试遵循 Arrange-Act-Assert(安排-执行-断言) 模型:
- Arrange(安排):设置测试条件
- Act(执行):调用某个函数或方法
- Assert(断言):验证最终条件是否成立
测试框架通常会拦截你的断言语句,以便在断言失败时提供信息。例如,unittest 提供了许多有用的断言工具。但即使是少量测试,也需要相当多的样板代码。
假设你想编写一个测试套件,仅用于确认项目中的 unittest 能正常工作。你可能想写一个始终通过的测试和一个始终失败的测试:
# test_with_unittest.py
from unittest import TestCase
class TryTesting(TestCase):
def test_always_passes(self):
self.assertTrue(True)
def test_always_fails(self):
self.assertTrue(False)
然后使用 unittest 的 discover 选项从命令行运行这些测试:
(venv) $ python -m unittest discover
F.
======================================================================
FAIL: test_always_fails (test_with_unittest.TryTesting)
----------------------------------------------------------------------
Traceback (most recent call last):
File "...\effective-python-testing-with-pytest\test_with_unittest.py", line 10, in test_always_fails
self.assertTrue(False)
AssertionError: False is not true
----------------------------------------------------------------------
Ran 2 tests in 0.006s
FAILED (failures=1)
如预期,一个测试通过,一个失败。你已证明 unittest 正常工作,但看看你做了什么:
- 从
unittest导入TestCase类 - 创建
TryTesting,它是TestCase的子类 - 为每个测试编写一个方法
- 使用
unittest.TestCase中的self.assert*方法进行断言
这是大量重复代码,而且是任何测试所需的最小代码量,你会反复编写相同的内容。
pytest 简化了这一流程:你可以直接使用普通函数和 Python 的 assert 关键字:
# test_with_pytest.py
def test_always_passes():
assert True
def test_always_fails():
assert False
就是这样!无需导入或类。只需定义一个以 test_ 开头的函数即可。由于可以直接使用 assert,你也不必学习或记住 unittest 中各种 self.assert* 方法。只要能写出一个期望为 True 的表达式,pytest 就会为你测试它。
pytest 不仅消除了大量样板代码,还提供了更详细、更易读的输出。
更友好的输出
你可以从项目根目录使用 pytest 命令运行测试套件:
(venv) $ pytest
============================= test session starts =============================
platform win32 -- Python 3.10.5, pytest-7.1.2, pluggy-1.0.0
rootdir: ...\effective-python-testing-with-pytest
collected 4 items
test_with_pytest.py .F [ 50%]
test_with_unittest.py F. [100%]
================================== FAILURES ===================================
______________________________ test_always_fails ______________________________
def test_always_fails():
> assert False
E assert False
test_with_pytest.py:7: AssertionError
________________________ TryTesting.test_always_fails _________________________
self = <test_with_unittest.TryTesting testMethod=test_always_fails>
def test_always_fails(self):
> self.assertTrue(False)
E AssertionError: False is not true
test_with_unittest.py:10: AssertionError
=========================== short test summary info ===========================
FAILED test_with_pytest.py::test_always_fails - assert False
FAILED test_with_unittest.py::TryTesting::test_always_fails - AssertionError:...
========================= 2 failed, 2 passed in 0.20s =========================
pytest 的输出方式与 unittest 不同,并且自动包含了 test_with_unittest.py 文件。报告包含:
- 系统状态(Python、pytest 和插件版本)
rootdir(搜索配置和测试的根目录)- 发现的测试数量
这些信息出现在输出的第一部分:
============================= test session starts =============================
platform win32 -- Python 3.10.5, pytest-7.1.2, pluggy-1.0.0
rootdir: ...\effective-python-testing-with-pytest
collected 4 items
随后用类似 unittest 的符号表示每个测试的状态:
.表示测试通过F表示测试失败E表示测试抛出未预期异常
符号旁显示测试名称,右侧显示整体进度:
test_with_pytest.py .F [ 50%]
test_with_unittest.py F. [100%]
对于失败的测试,报告会详细展示失败原因。例如,assert False 总是失败:
================================== FAILURES ===================================
______________________________ test_always_fails ______________________________
def test_always_fails():
> assert False
E assert False
test_with_pytest.py:7: AssertionError
...
这种额外输出在调试时非常有用。最后,报告给出整体状态:
=========================== short test summary info ===========================
FAILED test_with_pytest.py::test_always_fails - assert False
FAILED test_with_unittest.py::TryTesting::test_always_fails - AssertionError:...
========================= 2 failed, 2 passed in 0.20s =========================
与 unittest 相比,pytest 的输出信息更丰富、更易读。
更少的学习成本
能够使用 assert 关键字也非常强大。如果你之前用过它,那就无需学习新东西。以下是几个断言示例,展示你能编写哪些类型的测试:
# test_assert_examples.py
def test_uppercase():
assert "loud noises".upper() == "LOUD NOISES"
def test_reversed():
assert list(reversed([1, 2, 3, 4])) == [4, 3, 2, 1]
def test_some_primes():
assert 37 in {
num
for num in range(2, 50)
if not any(num % div == 0 for div in range(2, num))
}
它们看起来就像普通的 Python 函数。这使得 pytest 的学习曲线比 unittest 更平缓,因为你无需学习新构造即可上手。
注意:每个测试都很小且独立。这是常见做法——你会看到很长的函数名,但函数内部代码不多。这主要是为了保持测试彼此隔离,一旦出错,你能立即定位问题。附带的好处是输出中的标签更清晰。
提示:若想查看如何在主项目中创建测试套件的示例,请参阅《使用 TDD 在 Python 中构建哈希表》教程。你也可以通过 Python 练习题尝试测试驱动开发(TDD),为下一次面试或解析 CSV 文件做准备。
接下来,你将深入了解 pytest 如何利用 fixtures 管理测试输入值。
更易于管理状态和依赖
你的测试通常依赖于某些数据或测试替身(test doubles),例如模拟代码可能遇到的对象(如字典或 JSON 文件)。
在 unittest 中,你可能会将这些依赖提取到 .setUp() 和 .tearDown() 方法中,以便类中的每个测试都能使用它们。使用这些特殊方法没问题,但随着测试类变大,你可能会无意中使测试依赖变得完全隐式。换句话说,单独看某个测试时,你可能无法立即看出它依赖其他东西。
随着时间推移,隐式依赖可能导致代码纠缠不清,难以理解测试。而测试本应帮助你更好地理解代码。如果测试本身难以理解,那就有麻烦了!
pytest 采取了不同方法:它引导你进行显式依赖声明,同时通过 fixtures 保持可重用性。pytest fixtures 是可创建数据、测试替身或初始化系统状态的函数。任何需要使用 fixture 的测试都必须显式地将其作为参数传入测试函数,因此依赖关系始终一目了然:
# fixture_demo.py
import pytest
@pytest.fixture
def example_fixture():
return 1
def test_with_fixture(example_fixture):
assert example_fixture == 1
查看测试函数时,你能立即看出它依赖于某个 fixture,无需检查整个文件寻找 fixture 定义。
注意:通常应将测试放在项目根目录下的
tests文件夹中。
有关 Python 应用程序结构的更多信息,请参阅相关视频课程。
Fixtures 还可以使用其他 fixtures,同样通过显式声明依赖。这意味着随着时间推移,你的 fixtures 可以变得模块化。虽然将 fixtures 插入其他 fixtures 提供了极大的灵活性,但随着测试套件增长,管理依赖也可能更具挑战性。
稍后你将深入学习 fixtures,并尝试一些应对这些挑战的技术。
更容易过滤测试
随着测试套件增长,你可能只想运行某个功能的几个测试,将完整套件留到以后运行。pytest 提供了几种方法:
- 基于名称的过滤:使用
-k参数,仅运行全限定名匹配特定表达式的测试。 - 目录作用域:默认情况下,pytest 仅运行当前目录及其子目录中的测试。
- 测试分类:使用
-m参数,包含或排除你定义的特定类别的测试。
特别是测试分类是一个微妙而强大的工具。pytest 允许你为任意测试创建 marks(标记) 或自定义标签。一个测试可以有多个标签,你可以用它们精细控制要运行的测试。
稍后你将看到 pytest marks 的示例,并学习如何在大型测试套件中使用它们。
支持测试参数化
当你测试处理数据或执行通用转换的函数时,会发现自己编写了许多相似的测试。它们可能仅在输入或输出上有所不同。这会导致测试代码重复,有时甚至掩盖了你试图测试的行为。
unittest 提供了一种将多个测试合并为一个的方法,但它们在结果报告中不会显示为独立测试。如果一个测试失败而其余通过,整个组仍会返回单一失败结果。
pytest 提供了自己的解决方案:每个测试可以独立通过或失败。稍后你将学习如何使用 pytest 进行测试参数化。
基于插件的架构
pytest 最出色的功能之一是其对自定义和新功能的开放性。几乎每个组件都可以被修改。因此,pytest 用户开发了丰富的插件生态系统。
虽然有些 pytest 插件专注于特定框架(如 Django),但其他插件适用于大多数测试套件。稍后你将了解一些具体插件的详细信息。
Fixtures:管理状态和依赖
pytest fixtures 是一种向测试提供数据、测试替身或状态设置的方式。Fixtures 是可返回各种值的函数。每个依赖 fixture 的测试都必须显式接受该 fixture 作为参数。
何时创建 Fixtures
在本节中,你将模拟典型的测试驱动开发(TDD)工作流。
假设你正在编写一个函数 format_data_for_display(),用于处理 API 端点返回的数据。数据表示人员列表,每人包含名字(given_name)、姓氏(family_name)和职位(title)。该函数应输出字符串列表,格式为“全名: 职位”:
# format_data.py
def format_data_for_display(people):
... # 实现此函数!
按照良好的 TDD 习惯,你首先为其编写测试。你可能会编写以下代码:
# test_format_data.py
def test_format_data_for_display():
people = [
{
"given_name": "Alfonsa",
"family_name": "Ruiz",
"title": "Senior Software Engineer",
},
{
"given_name": "Sayid",
"family_name": "Khan",
"title": "Project Manager",
},
]
assert format_data_for_display(people) == [
"Alfonsa Ruiz: Senior Software Engineer",
"Sayid Khan: Project Manager",
]
在编写此测试时,你意识到可能还需要编写另一个函数,将数据转换为 Excel 可用的逗号分隔值(CSV):
# format_data.py
def format_data_for_display(people):
... # 实现此函数!
def format_data_for_excel(people):
... # 实现此函数!
待办事项增加了!这很好!TDD 的优势之一就是帮你规划后续工作。
format_data_for_excel() 函数的测试与 format_data_for_display() 非常相似:
# test_format_data.py
def test_format_data_for_display():
# ...
def test_format_data_for_excel():
people = [
{
"given_name": "Alfonsa",
"family_name": "Ruiz",
"title": "Senior Software Engineer",
},
{
"given_name": "Sayid",
"family_name": "Khan",
"title": "Project Manager",
},
]
assert format_data_for_excel(people) == """given,family,title
Alfonsa,Ruiz,Senior Software Engineer
Sayid,Khan,Project Manager
"""
值得注意的是,两个测试都重复定义了 people 变量,这占用了相当多的代码行。
如果你发现自己编写的多个测试都使用相同的底层测试数据,那么就该考虑使用 fixture 了。你可以将重复数据提取到一个用 @pytest.fixture 装饰的函数中,表明该函数是 pytest fixture:
# test_format_data.py
import pytest
@pytest.fixture
def example_people_data():
return [
{
"given_name": "Alfonsa",
"family_name": "Ruiz",
"title": "Senior Software Engineer",
},
{
"given_name": "Sayid",
"family_name": "Khan",
"title": "Project Manager",
},
]
你可以通过将 fixture 函数名作为参数添加到测试中来使用它。注意:不要调用 fixture 函数,pytest 会自动处理。你可以将 fixture 函数的返回值作为参数名使用:
# test_format_data.py
def test_format_data_for_display(example_people_data):
assert format_data_for_display(example_people_data) == [
"Alfonsa Ruiz: Senior Software Engineer",
"Sayid Khan: Project Manager",
]
def test_format_data_for_excel(example_people_data):
assert format_data_for_excel(example_people_data) == """given,family,title
Alfonsa,Ruiz,Senior Software Engineer
Sayid,Khan,Project Manager
"""
每个测试现在明显更短,但仍能清晰追溯到其所依赖的数据。务必为 fixture 起一个具体的名字,这样将来编写新测试时就能快速判断是否要使用它!
当你首次发现 fixtures 的强大功能时,可能会忍不住处处使用它们。但和所有事物一样,需要保持平衡。
何时避免使用 Fixtures
Fixtures 非常适合提取多个测试共用的数据或对象。然而,对于需要细微数据差异的测试,fixtures 并不总是最佳选择。在测试套件中随意添加 fixtures 并不比随意添加普通数据或对象更好,甚至可能因增加间接层而更糟。
和大多数抽象一样,找到合适的 fixture 使用程度需要一些练习和思考。
尽管如此,fixtures 很可能成为你测试套件的重要组成部分。随着项目规模扩大,扩展性挑战开始显现。任何工具面临的挑战之一是如何应对规模扩大带来的复杂性。幸运的是,pytest 提供了许多有用的功能来帮助你管理这种复杂性。
如何在大规模项目中使用 Fixtures
当你从测试中提取更多 fixtures 时,可能会发现某些 fixtures 可进一步抽象。在 pytest 中,fixtures 是模块化的。这意味着 fixtures 可以被导入、导入其他模块,并且可以依赖和导入其他 fixtures。这一切允许你为自己的用例组合出合适的 fixture 抽象。
例如,你可能会发现两个不同文件(或模块)中的 fixtures 共享一个公共依赖。此时,你可以将 fixtures 从测试模块移到更通用的 fixture 相关模块中。这样,你就可以在任何需要它们的测试模块中导入它们。当你在整个项目中反复使用某个 fixture 时,这是一个好方法。
如果你想让某个 fixture 在整个项目中可用而无需导入,可以使用名为 conftest.py 的特殊配置模块。
pytest 会在每个目录中查找 conftest.py 模块。如果你将通用 fixtures 添加到 conftest.py 模块中,那么你就可以在该模块的父目录及所有子目录中使用该 fixture,而无需导入。这是放置最常用 fixtures 的绝佳位置。
fixtures 和 conftest.py 的另一个有趣用例是保护资源访问。假设你为处理 API 调用的代码编写了测试套件。你想确保测试套件不会发出任何真实的网络请求,即使有人不小心编写了这样的测试。
pytest 提供了 monkeypatch fixture 来替换值和行为,你可以很好地利用它:
# conftest.py
import pytest
import requests
@pytest.fixture(autouse=True)
def disable_network_calls(monkeypatch):
def stunted_get():
raise RuntimeError("Network access not allowed during testing!")
monkeypatch.setattr(requests, "get", lambda *args, **kwargs: stunted_get())
通过将 disable_network_calls() 放在 conftest.py 中并添加 autouse=True 选项,你确保了在整个测试套件中禁用网络调用。任何执行调用 requests.get() 的测试都会引发 RuntimeError,表明发生了意外的网络调用。
你的测试套件数量不断增长,这让你在修改代码时更有信心,不会意外破坏功能。但随着测试套件增长,运行时间可能变长。即使运行时间不长,也许你正专注于某个核心行为,而该行为会影响大多数测试。在这种情况下,你可能希望将测试运行器限制为仅运行特定类别的测试。
Marks:测试分类
在任何大型测试套件中,当你快速迭代新功能时,最好避免运行所有测试。除了 pytest 默认运行当前工作目录中所有测试的行为,或使用过滤功能外,你还可以利用 markers(标记)。
pytest 允许你为测试定义类别,并在运行套件时提供包含或排除类别的选项。你可以为测试标记任意数量的类别。
标记测试对于按子系统或依赖关系对测试进行分类非常有用。例如,如果你的一些测试需要访问数据库,可以为它们创建 @pytest.mark.database_access 标记。
专业提示:因为你可以为标记使用任意名称,所以很容易拼错或记错标记名称。pytest 会在测试输出中警告你它不认识的标记。
你可以使用--strict-markers标志确保所有标记都在 pytest 配置文件pytest.ini中注册。这会阻止你运行测试,直到你注册所有未知标记。
有关注册标记的更多信息,请参阅 pytest 文档。
运行测试时,你仍然可以使用 pytest 命令运行所有测试。如果你想只运行需要数据库访问的测试,可以使用 pytest -m database_access。要运行除数据库访问外的所有测试,可以使用 pytest -m "not database_access"。你甚至可以使用 autouse fixture 将数据库访问限制为仅标记为 database_access 的测试。
一些插件通过添加自己的守卫(guards)扩展了标记的功能。例如,pytest-django 插件提供了 django_db 标记。任何未标记此标记而尝试访问数据库的测试都会失败。第一个尝试访问数据库的测试会触发 Django 测试数据库的创建。
要求添加 django_db 标记的做法促使你显式声明依赖关系。毕竟,这就是 pytest 的哲学!这也意味着你可以更快地运行不依赖数据库的测试,因为 pytest -m "not django_db" 会阻止测试触发数据库创建。节省的时间累积起来非常可观,尤其是当你勤于频繁运行测试时。
pytest 自带几个标记:
skip:无条件跳过测试。skipif:如果传递给它的表达式求值为True,则跳过测试。xfail:表示测试预期会失败,因此即使测试失败,整个套件仍可成功。parametrize:使用不同参数值创建测试的多个变体。稍后你将了解更多。
你可以通过运行 pytest --markers 查看 pytest 认识的所有标记。
说到参数化,接下来就是它了。
参数化:合并测试
你之前看到 pytest fixtures 如何通过提取公共依赖来减少代码重复。当多个测试具有略微不同的输入和预期输出时,fixtures 并不太适用。在这种情况下,你可以对单个测试定义进行参数化,pytest 会根据你指定的参数为你创建测试变体。
假设你编写了一个函数来判断字符串是否为回文。初始测试集可能如下所示:
def test_is_palindrome_empty_string():
assert is_palindrome("")
def test_is_palindrome_single_character():
assert is_palindrome("a")
def test_is_palindrome_mixed_casing():
assert is_palindrome("Bob")
def test_is_palindrome_with_spaces():
assert is_palindrome("Never odd or even")
def test_is_palindrome_with_punctuation():
assert is_palindrome("Do geese see God?")
def test_is_palindrome_not_palindrome():
assert not is_palindrome("abc")
def test_is_palindrome_not_quite():
assert not is_palindrome("abab")
除了最后两个,所有测试都有相同的结构:
def test_is_palindrome_<某种情况>():
assert is_palindrome("<某个字符串>")
这开始闻起来像样板代码了。pytest 到目前为止一直帮你消除样板代码,现在也不会让你失望。你可以使用 @pytest.mark.parametrize() 用不同值填充这个结构,大幅减少测试代码:
@pytest.mark.parametrize("palindrome", [
"",
"a",
"Bob",
"Never odd or even",
"Do geese see God?",
])
def test_is_palindrome(palindrome):
assert is_palindrome(palindrome)
@pytest.mark.parametrize("non_palindrome", [
"abc",
"abab",
])
def test_is_palindrome_not_palindrome(non_palindrome):
assert not is_palindrome(non_palindrome)
parametrize() 的第一个参数是以逗号分隔的参数名字符串。你不必提供多个名称,如本例所示。第二个参数是元组或单值列表,表示参数值。
你可以进一步参数化,将所有测试合并为一个:
@pytest.mark.parametrize("maybe_palindrome, expected_result", [
("", True),
("a", True),
("Bob", True),
("Never odd or even", True),
("Do geese see God?", True),
("abc", False),
("abab", False),
])
def test_is_palindrome(maybe_palindrome, expected_result):
assert is_palindrome(maybe_palindrome) == expected_result
虽然这缩短了代码,但需要注意:在这种情况下,你实际上失去了一些原始函数更具描述性的特点。确保不要将测试套件参数化到难以理解的程度。你可以使用参数化将测试数据与测试行为分离,使测试目的清晰,同时使不同测试用例更易读和维护。
持续时间报告:对抗慢速测试
每次从实现代码切换到测试代码时,都会产生一些开销。如果测试本身就很慢,这种开销会导致摩擦和挫败感。
你之前读到过使用标记在运行套件时过滤掉慢速测试,但总有一天你需要运行它们。如果你想提高测试速度,了解哪些测试可能带来最大改进会很有用。pytest 可以自动记录测试持续时间并报告最慢的测试。
使用 --durations 选项向 pytest 命令添加持续时间报告。--durations 期望一个整数值 n,并报告最慢的 n 个测试。测试报告中将包含一个新部分:
(venv) $ pytest --durations=5
...
============================= slowest 5 durations =============================
3.03s call test_code.py::test_request_read_timeout
1.07s call test_code.py::test_request_connection_timeout
0.57s call test_code.py::test_database_read
(2 durations < 0.005s hidden. Use -vv to show these durations.)
=========================== short test summary info ===========================
...
持续时间报告中出现的每个测试都是加速的好候选者,因为它们占用了高于平均水平的总测试时间。注意,默认情况下短持续时间会被隐藏。如报告所述,你可以结合 -vv 传递 --durations 来增加报告详细程度并显示这些短持续时间。
请注意,某些测试可能有不可见的设置开销。你之前读到过,第一个标记为 django_db 的测试会触发 Django 测试数据库的创建。持续时间报告会将数据库创建时间反映在触发数据库创建的测试中,这可能会产生误导。
你已接近完整的测试覆盖率。接下来,你将了解 pytest 丰富插件生态系统中的一些插件。
有用的 pytest 插件
你之前在本教程中了解了一些有价值的 pytest 插件。在本节中,你将更深入地探索这些插件以及其他插件——从 pytest-randomly 等实用插件到 Django 等库的专用插件。
pytest-randomly
通常测试顺序并不重要,但随着代码库增长,你可能会无意中引入一些副作用,导致某些测试在乱序运行时失败。
pytest-randomly 强制你的测试以随机顺序运行。pytest 总是在运行前收集所有能找到的测试。pytest-randomly 只是在执行前打乱这个测试列表。
这是发现依赖特定运行顺序的测试的好方法,这意味着它们对其他测试有状态依赖。如果你从头开始在 pytest 中构建测试套件,这种情况不太可能发生。在迁移到 pytest 的测试套件中更可能发生。
该插件会在配置描述中打印一个种子值。你可以使用该值在尝试修复问题时以相同顺序运行测试。
pytest-cov
如果你想衡量测试对实现代码的覆盖程度,可以使用 coverage 包。pytest-cov 集成了 coverage,因此你可以运行 pytest --cov 查看测试覆盖率报告,并在项目主页上炫耀它。
pytest-django
pytest-django 为 Django 测试提供了一些有用的 fixtures 和标记。你之前在本教程中见过 django_db 标记。rf fixture 提供对 Django RequestFactory 实例的直接访问。settings fixture 提供快速设置或覆盖 Django 设置的方法。这些插件能极大提升你的 Django 测试效率!
如果你有兴趣深入了解在 Django 中使用 pytest,请参阅《如何在 Pytest 中为 Django 模型提供测试 Fixtures》。
pytest-bdd
pytest 可用于运行超出传统单元测试范围的测试。行为驱动开发(BDD)鼓励编写用户操作和期望的纯文本描述,然后用它们确定是否实现某个功能。pytest-bdd 帮助你使用 Gherkin 为代码编写特性测试。
你可以通过这份详尽的第三方插件列表查看 pytest 的其他可用插件。
结论
pytest 提供了一套核心生产力功能,用于过滤和优化测试,以及一个灵活的插件系统,进一步扩展其价值。无论你拥有庞大的遗留 unittest 套件,还是从头开始新项目,pytest 都能为你提供帮助。
在本教程中,你学习了如何使用:
- Fixtures 处理测试依赖、状态和可重用功能
- Marks 对测试进行分类并限制对外部资源的访问
- Parametrization 减少测试间的重复代码
- Durations 识别最慢的测试
- Plugins 与其他框架和测试工具集成
安装 pytest 并尝试一下吧。你会庆幸自己这么做的。祝你测试愉快!