pytest夹具
现在你已经用pytest编写和运行了测试函数,让我们把注意力转移到称为fixtures的测试辅助函数上,它对几乎所有非微不足道的软件系统的测试代码结构都是至关重要的。fixtures是在实际测试函数之前(有时是之后)由pytest运行的函数。固定程序中的代码可以做任何你想做的事情。你可以使用fixtures来获取测试所需的数据集。你可以使用fixtures在运行测试之前让系统进入已知的状态。fixtures也被用来为多个测试准备数据。
在本章中,你将学习如何创建fixtures,并学习如何使用它们。
Fixture入门
ch3/test_fixtures.py
import pytest
@pytest.fixture()
def some_data():
"""Return answer to ultimate question."""
return 42
def test_some_data(some_data):
"""Use fixture return value in a test."""
assert some_data == 42
@pytest.fixture()装饰器用于告诉pytest此函数是Fixture。pytest在运行测试之前要运行它。Fixture可以做工作,也可以向测试函数返回数据。
pytest使用装饰器来为其他函数添加功能和特性。在本例中pytest.fixture() 装饰 some_data() 函数。test_some_data()将some_data作为参数。
在编程和测试社区,甚至在 Python 社区,术语Fixture 有很多含义。我交替使用 "fixture"、"fixture函数 "和 "fixture方法 "来指称本章中讨论的@pytest.fixture()装饰的函数。Fixture也可以用来指代被fixture函数设置的资源。fixture函数经常设置或检索一些测试可以使用的数据。有时这些数据被认为是一个夹具。例如,Django社区经常使用fixture来指一些在应用程序开始时被加载到数据库的初始数据。
不管其他含义如何,在pytest和本书中,测试夹具指的是pytest提供的机制,允许从你的测试函数中分离出 "准备 "和 "清理 "代码。
与测试函数相比,pytest在Fixture中对异常的处理方式不同。在测试代码中发生的异常(或断言失败或对pytest.fail()的调用)会导致 "失败 "的结果。然而,在Fixture中,测试函数被报告为 "错误"。这个区别在调试为什么测试没有通过时是有帮助的。如果测试的结果是 "失败",那么失败是在测试函数的某个地方(或函数调用的东西)。如果测试的结果是 "错误",那么故障是在Fixture的某个地方。
pytest Fixture是使pytest在其他测试框架中脱颖而出的独特的核心功能之一,也是许多人转向并留在pytest的原因。关于Fixture有很多特点和细微差别。一旦你对它们的工作方式有了良好的心理模型,它们对你来说就会显得很容易。然而,你必须玩一段时间才能达到这个目的,所以接下来让我们来做这个。
使用Fixture进行Setup和Teardown
Fixture将对我们测试Cards应用程序有很大的帮助。
ch3/test_count_initial.py
from pathlib import Path
from tempfile import TemporaryDirectory
import cards
import pytest
@pytest.fixture()
def cards_db():
with TemporaryDirectory() as db_dir:
db_path = Path(db_dir)
db = cards.CardsDB(db_path)
yield db
db.close()
def test_empty(cards_db):
assert cards_db.count() == 0
def test_two(cards_db):
cards_db.add_card(cards.Card("first"))
cards_db.add_card(cards.Card("second"))
assert cards_db.count() == 2
为了调用 count() ,我们需要一个数据库对象,我们通过调用 cards.CardsDB(db_path) 得到它。cards.CardsDB() 函数是构造函数;它返回CardsDB 对象。db_path 参数需要是一个 pathlib.Path 对象,指向数据库目录。pathlib 模块在 Python 3.4 中引入,pathlib.Path对象是表示文件系统路径的标准方式。对于测试来说,临时目录是可行的,我们从tempfile.TemporaryDirectory()中得到。
这个测试函数真的不是很痛苦。它只有几行代码。让我们来看看这些问题。在我们调用count()之前,有一些代码是用来设置数据库的,这并不是我们真正想要测试的东西。在断言语句前有对db.close()的调用。把它放在函数的最后似乎更好,但我们必须在assert之前调用它,因为如果assert语句失败,它就不会被调用。
Fixture在使用它们的测试之前运行。如果在函数中出现了yield,它就在那里停止,将控制权传给测试,并在测试完成后继续运行。
在我们的例子中,产生发生在一个带有临时目录块的上下文管理器中。该目录在夹具使用和测试运行时保持不变。测试结束后,控制权回到夹具上,db.close()可以运行,然后with块可以完成并清理目录。
用-setup-show追踪Fixture的执行情况
pytest提供了命令行标志--setup-show,它可以显示测试和Fixture的操作顺序。
$ pytest --setup-show test_count.py
============================= test session starts =============================
platform win32 -- Python 3.9.13, pytest-7.1.2, pluggy-1.0.0
rootdir: D:\code\pytest_quick, configfile: pytest.ini
plugins: allure-pytest-2.12.0, Faker-4.18.0, tep-0.8.2, anyio-3.5.0
collected 2 items
test_count.py
SETUP S _session_faker
SETUP F cards_db
ch3/test_count.py::test_empty (fixtures used: _session_faker, cards_db, request).
TEARDOWN F cards_db
SETUP F cards_db
ch3/test_count.py::test_two (fixtures used: _session_faker, cards_db, request).
TEARDOWN F cards_db
TEARDOWN S _session_faker
============================== 2 passed in 0.17s ==============================
指定Fixture范围
每个Fixture都有特定的范围,它定义了相对于使用该Fixture的所有测试功能的运行,设置和拆除的顺序。当它被多个测试功能使用时,范围决定了设置和拆除的运行频率。
Fixture的默认范围是函数范围。这意味着夹具的设置部分将在每个需要它的测试运行之前运行。同样地,在测试完成后,每个测试的拆解部分也会运行。
然而,有时你可能不希望发生这种情况。也许设置和连接数据库很耗时,或者你正在生成大量的数据集,或者你正在从服务器或慢速设备中检索数据。真的,你可以在一个Fixture内做任何你想做的事情,而其中一些可能很慢。
让我们改变我们的Fixture的范围,使数据库只被打开一次,在fixture装饰器中添加scope="module"。
test_mod_scope.py
from pathlib import Path
from tempfile import TemporaryDirectory
import cards
import pytest
@pytest.fixture(scope="module")
def cards_db():
with TemporaryDirectory() as db_dir:
db_path = Path(db_dir)
db = cards.CardsDB(db_path)
yield db
db.close()
def test_empty(cards_db):
assert cards_db.count() == 0
def test_non_empty(cards_db):
cards_db.add_card(cards.Card("first"))
cards_db.add_card(cards.Card("second"))
assert cards_db.count() == 2
fixture装饰器的范围参数不只function和module。还有class,package和session。默认的范围是function。
对于定义在测试模块中的fixture,会话和包的作用域与模块作用域一样。为了使用这些其他作用域,我们需要把它们放在conftest.py文件中。