另请参阅
关于 fixture
另请参阅
Fixture 参考
请求 fixture
最基本的用法是:测试函数把所需 fixture 声明为参数,以此请求 fixture。
pytest 运行测试时,会查看测试函数签名中的参数,再寻找与参数同名的 fixture。找到后,pytest 执行这些 fixture,取得其返回值(如果有),并把这些对象作为参数传给测试函数。
简短示例
import pytest
class Fruit:
def __init__(self, name):
self.name = name
self.cubed = False
def cube(self):
self.cubed = True
class FruitSalad:
def __init__(self, *fruit_bowl):
self.fruit = fruit_bowl
self._cube_fruit()
def _cube_fruit(self):
for fruit in self.fruit:
fruit.cube()
# Arrange
@pytest.fixture
def fruit_bowl():
return [Fruit("apple"), Fruit("banana")]
def test_fruit_salad(fruit_bowl):
# Act
fruit_salad = FruitSalad(*fruit_bowl)
# Assert
assert all(fruit.cubed for fruit in fruit_salad.fruit)
本例中,test_fruit_salad 请求 fruit_bowl,也就是 def test_fruit_salad(fruit_bowl):。pytest 看到这一声明,就会执行 fruit_bowl fixture 函数,把返回对象作为 fruit_bowl 参数传给 test_fruit_salad。
如果手工完成这一过程,大致如下:
def fruit_bowl():
return [Fruit("apple"), Fruit("banana")]
def test_fruit_salad(fruit_bowl):
# Act
fruit_salad = FruitSalad(*fruit_bowl)
# Assert
assert all(fruit.cubed for fruit in fruit_salad.fruit)
# Arrange
bowl = fruit_bowl()
test_fruit_salad(fruit_bowl=bowl)
Fixture 可以请求其他 fixture
pytest 的一大优势是极其灵活的 fixture 系统。我们可以把复杂的测试需求拆解为简单、有组织的函数,让每个函数只需声明自己的依赖。后文会进一步介绍;这里先用一个简短示例说明 fixture 怎样使用其他 fixture:
# contents of test_append.py
import pytest
# Arrange
@pytest.fixture
def first_entry():
return "a"
# Arrange
@pytest.fixture
def order(first_entry):
return [first_entry]
def test_string(order):
# Act
order.append("b")
# Assert
assert order == ["a", "b"]
注意,这与上面的示例相同,变化很少。pytest 中的 fixture 像测试一样请求 fixture;测试所适用的请求规则也同样适用于 fixture。如果手工完成,过程如下:
def first_entry():
return "a"
def order(first_entry):
return [first_entry]
def test_string(order):
# Act
order.append("b")
# Assert
assert order == ["a", "b"]
entry = first_entry()
the_list = order(first_entry=entry)
test_string(order=the_list)
Fixture 可以复用
fixture 系统强大的原因之一是:可以像普通函数一样,把通用的准备步骤定义一次,再反复使用。两个不同的测试可以请求同一个 fixture,而 pytest 会分别给每个测试提供该 fixture 的结果。
这对于防止测试彼此影响非常有用。通过这一系统,每个测试都能获得新的一批数据,从干净状态开始,从而提供一致、可重复的结果。
下面展示这种机制的用处:
# contents of test_append.py
import pytest
# Arrange
@pytest.fixture
def first_entry():
return "a"
# Arrange
@pytest.fixture
def order(first_entry):
return [first_entry]
def test_string(order):
# Act
order.append("b")
# Assert
assert order == ["a", "b"]
def test_int(order):
# Act
order.append(2)
# Assert
assert order == ["a", 2]
这里每个测试都取得自己的 list 对象,因此 order fixture 执行两次,first_entry fixture 也同样如此。手工完成的过程如下:
def first_entry():
return "a"
def order(first_entry):
return [first_entry]
def test_string(order):
# Act
order.append("b")
# Assert
assert order == ["a", "b"]
def test_int(order):
# Act
order.append(2)
# Assert
assert order == ["a", 2]
entry = first_entry()
the_list = order(first_entry=entry)
test_string(order=the_list)
entry = first_entry()
the_list = order(first_entry=entry)
test_int(order=the_list)
测试或 fixture 可以同时请求多个 fixture
测试和 fixture 一次请求的数量不限于一个,而是可以任意多个。下面再用一个简短示例说明:
# contents of test_append.py
import pytest
# Arrange
@pytest.fixture
def first_entry():
return "a"
# Arrange
@pytest.fixture
def second_entry():
return 2
# Arrange
@pytest.fixture
def order(first_entry, second_entry):
return [first_entry, second_entry]
# Arrange
@pytest.fixture
def expected_list():
return ["a", 2, 3.0]
def test_string(order, expected_list):
# Act
order.append(3.0)
# Assert
assert order == expected_list
同一测试中可以多次请求 fixture:返回值会缓存
同一个测试中,也可以多次请求同一 fixture;pytest 不会因此再次执行它。多个依赖于同一 fixture 的 fixture 可以请求它,测试本身也可以再次请求,而它只执行一次。
# contents of test_append.py
import pytest
# Arrange
@pytest.fixture
def first_entry():
return "a"
# Arrange
@pytest.fixture
def order():
return []
# Act
@pytest.fixture
def append_first(order, first_entry):
return order.append(first_entry)
def test_string_only(append_first, order, first_entry):
# Assert
assert order == [first_entry]
如果每次请求都执行 fixture,这个测试就会失败,因为 append_first 和 test_string_only 看到的 order 都会是空列表 []。不过,order 首次调用后,其返回值以及执行产生的副作用都被缓存;测试和 append_first 引用的是同一对象,因此测试能看到 append_first 对它的影响。
Autouse fixture:无需显式请求的 fixture
有时你知道所有测试都会依赖某个或多个 fixture。Autouse fixture 可以让所有测试自动请求它们,减少重复请求,也能支持更复杂的用法,后文将继续介绍。
向 fixture 装饰器传入 autouse=True,就能把它变成 autouse fixture。下面是一个简单示例:
# contents of test_append.py
import pytest
@pytest.fixture
def first_entry():
return "a"
@pytest.fixture
def order(first_entry):
return []
@pytest.fixture(autouse=True)
def append_first(order, first_entry):
return order.append(first_entry)
def test_string_only(order, first_entry):
assert order == [first_entry]
def test_string_and_int(order, first_entry):
order.append(2)
assert order == [first_entry, 2]
本例的 append_first 是 autouse fixture,因此两个测试即使没有请求它,也会自动受其影响。这不意味着不能显式请求它,只意味着没有必要。
作用域:在类、模块、包或会话之间共享 fixture
需要网络访问的 fixture 依赖连接,而且创建通常耗时。延续前面的例子,可以给 @pytest.fixture 加上 scope=”module”,使负责连接已有 SMTP 服务器的 smtp_connection fixture 每个测试模块只执行一次,而默认是每个测试函数执行一次。同一模块内的多个测试函数会获得同一个 smtp_connection 实例,以节约时间。scope 可为 function、class、module、package 或 session。
下一示例把 fixture 函数放到独立的 conftest.py 文件中,让同一目录内多个测试模块都能访问它:
# content of conftest.py
import smtplib
import pytest
@pytest.fixture(scope="module")
def smtp_connection():
return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
# content of test_module.py
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
assert b"smtp.gmail.com" in msg
assert 0 # for demo purposes
def test_noop(smtp_connection):
response, msg = smtp_connection.noop()
assert response == 250
assert 0 # for demo purposes
这里 test_ehlo 需要 smtp_connection 的值。pytest 会发现并调用被 @pytest.fixture 标记的 smtp_connection 函数。运行结果如下:
$ pytest test_module.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-9.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 2 items
test_module.py FF [100%]
================================= FAILURES =================================
________________________________ test_ehlo _________________________________
smtp_connection = <smtplib.SMTP object at 0xdeadbeef0001>
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
assert b"smtp.gmail.com" in msg
> assert 0 # for demo purposes
^^^^^^^^
E assert 0
test_module.py:7: AssertionError
________________________________ test_noop _________________________________
smtp_connection = <smtplib.SMTP object at 0xdeadbeef0001>
def test_noop(smtp_connection):
response, msg = smtp_connection.noop()
assert response == 250
> assert 0 # for demo purposes
^^^^^^^^
E assert 0
test_module.py:13: AssertionError
========================= short test summary info ==========================
FAILED test_module.py::test_ehlo - assert 0
FAILED test_module.py::test_noop - assert 0
============================ 2 failed in 0.12s =============================
两个 assert 0 都失败了;更关键的是,回溯显示传入参数的值,可以看出两个测试收到完全相同的 smtp_connection 对象。由于复用了实例,使用 smtp_connection 的两个测试可以像一个测试一样迅速执行。
如果你希望 smtp_connection 的作用域是整个会话,只需这样声明:
@pytest.fixture(scope="session")
def smtp_connection():
# the returned fixture value will be shared for
# all tests requesting it
...
Fixture 作用域
Fixture 在测试首次请求时创建,并根据 scope 决定销毁时机:
-
function:默认作用域,在测试结束时销毁。
-
class:在类中最后一个测试的清理阶段销毁。
-
module:在模块中最后一个测试的清理阶段销毁。
-
package:在定义 fixture 的包中最后一个测试的清理阶段销毁;包含其中的子包和子目录。
-
session:在测试会话结束时销毁。
注意
pytest 同一时间只缓存一个 fixture 实例;因此,对参数化 fixture 来说,在给定作用域内仍可能执行多次。
动态作用域
版本 5.2 新增。
有时你希望改变 fixture 作用域而不改动代码。可以把可调用对象传给 scope。它必须返回有效作用域名称的字符串,且只在 fixture 定义时执行一次。调用时接收两个关键字参数:字符串 fixture_name,以及配置对象 config。
这对于准备阶段耗时的 fixture 尤其有用,例如启动 Docker 容器。你可以用命令行参数控制不同环境中的容器作用域。示例如下。
def determine_scope(fixture_name, config):
if config.getoption("--keep-containers", None):
return "session"
return "function"
@pytest.fixture(scope=determine_scope)
def docker_container():
yield spawn_container()
拆除与清理:fixture 的终结处理
运行测试时,应确保测试自行清理,既不干扰其他测试,也不在系统里遗留大量测试数据。pytest fixture 提供了有用的清理系统,让每个 fixture 都能定义自身需要的具体清理步骤。
这个系统有两种用法。
1. yield fixture:推荐
yield fixture 使用 yield 而非 return。像其他 fixture 一样,可以执行代码并把对象交给请求它的 fixture 或测试。区别只有两点:
-
把 return 换成 yield。
-
把该 fixture 的所有清理代码放在 yield 后面。
pytest 确定 fixture 的线性执行顺序后,会依次执行每个 fixture,直到 return 或 yield,再执行下一个。
测试结束后,pytest 按相反顺序遍历列表,找到每个使用 yield 的 fixture,执行其 yield 后面的代码。
举一个简单的例子,考虑下面的基本邮件模块:
# content of emaillib.py
class MailAdminClient:
def create_user(self):
return MailUser()
def delete_user(self, user):
# do some cleanup
pass
class MailUser:
def __init__(self):
self.inbox = []
def send_email(self, email, other):
other.inbox.append(email)
def clear_mailbox(self):
self.inbox.clear()
class Email:
def __init__(self, subject, body):
self.subject = subject
self.body = body
假设要测试用户之间发送邮件,须先创建两个用户,再从一个用户给另一个发邮件,最后断言收件箱中收到消息。测试结束后,如果要清理,可能得先清空收件箱再删除收件用户,否则系统可能报错。
实现可能如下:
# content of test_emaillib.py
from emaillib import Email, MailAdminClient
import pytest
@pytest.fixture
def mail_admin():
return MailAdminClient()
@pytest.fixture
def sending_user(mail_admin):
user = mail_admin.create_user()
yield user
mail_admin.delete_user(user)
@pytest.fixture
def receiving_user(mail_admin):
user = mail_admin.create_user()
yield user
user.clear_mailbox()
mail_admin.delete_user(user)
def test_email_received(sending_user, receiving_user):
email = Email(subject="Hey!", body="How's it going?")
sending_user.send_email(email, receiving_user)
assert email in receiving_user.inbox
由于 receiving_user 在准备阶段最后执行,它会在清理阶段最先执行。
即使清理顺序正确,也不能保证清理一定安全。安全清理一节会进一步讨论这种风险。
$ pytest -q test_emaillib.py
. [100%]
1 passed in 0.12s
处理 yield fixture 的错误
如果 yield fixture 在执行 yield 前抛出异常,pytest 不会尝试执行它的 yield 后面的清理代码。但对于该测试中此前已成功执行的每个 fixture,pytest 仍会照常尝试清理。
2. 直接添加 finalizer
yield fixture 通常更简洁、直观,但也可以直接向测试的 request-context 对象添加 finalizer 函数。效果类似,只是代码更冗长。
要采用这一方式,在需要清理代码的 fixture 中像请求其他 fixture 一样请求 request-context 对象,再把包含清理代码的可调用对象传给其 addfinalizer 方法。
需要小心:finalizer 一旦加入,即使该 fixture 随后抛出异常,pytest 也会执行它。因此,只有在 fixture 已完成确实需要清理的操作后,才应添加 finalizer,避免执行不必要的清理。
使用 addfinalizer 的前一个示例如下:
# content of test_emaillib.py
from emaillib import Email, MailAdminClient
import pytest
@pytest.fixture
def mail_admin():
return MailAdminClient()
@pytest.fixture
def sending_user(mail_admin):
user = mail_admin.create_user()
yield user
mail_admin.delete_user(user)
@pytest.fixture
def receiving_user(mail_admin, request):
user = mail_admin.create_user()
def delete_user():
mail_admin.delete_user(user)
request.addfinalizer(delete_user)
return user
@pytest.fixture
def email(sending_user, receiving_user, request):
_email = Email(subject="Hey!", body="How's it going?")
sending_user.send_email(_email, receiving_user)
def empty_mailbox():
receiving_user.clear_mailbox()
request.addfinalizer(empty_mailbox)
return _email
def test_email_received(receiving_user, email):
assert email in receiving_user.inbox
比 yield fixture 更长,也更复杂,但在遇到特殊情况时,可以提供更细致的控制。
$ pytest -q test_emaillib.py
. [100%]
1 passed in 0.12s
关于 finalizer 的执行顺序
Finalizer 采用先进后出顺序。对于 yield fixture,最右侧 fixture,也就是最后一个测试参数,对应的清理代码最先执行。
# content of test_finalizers.py
import pytest
def test_bar(fix_w_yield1, fix_w_yield2):
print("test_bar")
@pytest.fixture
def fix_w_yield1():
yield
print("after_yield_1")
@pytest.fixture
def fix_w_yield2():
yield
print("after_yield_2")
$ pytest -s test_finalizers.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-9.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 1 item
test_finalizers.py test_bar
.after_yield_2
after_yield_1
============================ 1 passed in 0.12s =============================
对于直接添加的 finalizer,最先执行的是最后一次 request.addfinalizer 注册的函数。
# content of test_finalizers.py
from functools import partial
import pytest
@pytest.fixture
def fix_w_finalizers(request):
request.addfinalizer(partial(print, "finalizer_2"))
request.addfinalizer(partial(print, "finalizer_1"))
def test_bar(fix_w_finalizers):
print("test_bar")
$ pytest -s test_finalizers.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-9.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 1 item
test_finalizers.py test_bar
.finalizer_1
finalizer_2
============================ 1 passed in 0.12s =============================
这是因为 yield fixture 在底层也使用 addfinalizer:fixture 执行时,addfinalizer 注册一个恢复生成器的函数,后者再调用清理代码。
安全清理
pytest fixture 系统很强大,但它毕竟由计算机执行,无法自行判断我们交给它的所有资源该如何安全拆除。如果不够小心,在错误位置发生异常可能遗留测试资源,很快引发更多问题。
例如,考虑基于前面邮件示例的以下测试:
# content of test_emaillib.py
from emaillib import Email, MailAdminClient
import pytest
@pytest.fixture
def setup():
mail_admin = MailAdminClient()
sending_user = mail_admin.create_user()
receiving_user = mail_admin.create_user()
email = Email(subject="Hey!", body="How's it going?")
sending_user.send_email(email, receiving_user)
yield receiving_user, email
receiving_user.clear_mailbox()
mail_admin.delete_user(sending_user)
mail_admin.delete_user(receiving_user)
def test_email_received(setup):
receiving_user, email = setup
assert email in receiving_user.inbox
这一版本更紧凑,但更难阅读,fixture 名称也不够明确,而且各 fixture 都不易复用。
更严重的问题是:准备阶段任何一步抛出异常,都不会执行清理代码。
可以考虑改用 addfinalizer 而非 yield fixture,但可能变得相当复杂、难以维护,也不再紧凑。
$ pytest -q test_emaillib.py
. [100%]
1 passed in 0.12s
安全的 fixture 结构
最安全、简单的结构是让每个 fixture 只做一个改变状态的操作,再与对应的清理代码放在一起,如前面的邮件示例所示。
改变状态的操作在失败后仍改变状态的概率很低,因为大多数这类操作往往基于事务,至少在可能遗留状态的测试层面如此。因此,把每个成功的状态变更移入独立 fixture,让它与其他可能失败的状态变更分开,并确保它有对应清理,将最有助于测试把环境恢复到开始时的状态。
例如,有一个带登录页的网站,也有可以创建用户的管理 API。测试需要:
-
通过管理 API 创建用户。
-
使用 Selenium 启动浏览器。
-
访问网站登录页。
-
以刚创建的用户登录。
-
断言登录后页面的页头中包含该用户的姓名。
我们不希望遗留用户,也不希望浏览器会话一直运行,因此负责创建这些资源的 fixture 应负责清理。
实现可能如下:
注意
此例假定某些 fixture,例如 base_url 与 admin_credentials,已在其他地方定义。这里暂且假定它们存在,只是不展示。
from uuid import uuid4
from urllib.parse import urljoin
from selenium.webdriver import Chrome
import pytest
from src.utils.pages import LoginPage, LandingPage
from src.utils import AdminApiClient
from src.utils.data_types import User
@pytest.fixture
def admin_client(base_url, admin_credentials):
return AdminApiClient(base_url, **admin_credentials)
@pytest.fixture
def user(admin_client):
_user = User(name="Susan", username=f"testuser-{uuid4()}", password="P4$$word")
admin_client.create_user(_user)
yield _user
admin_client.delete_user(_user)
@pytest.fixture
def driver():
_driver = Chrome()
yield _driver
_driver.quit()
@pytest.fixture
def login(driver, base_url, user):
driver.get(urljoin(base_url, "/login"))
page = LoginPage(driver)
page.login(user)
@pytest.fixture
def landing_page(driver, login):
return LandingPage(driver)
def test_name_on_landing_page_after_login(landing_page, user):
assert landing_page.header == f"Welcome, {user.name}!"
从依赖关系看,不确定 user 是否先于 driver 执行。但这没关系,因为它们是原子操作,先后顺序不影响测试事件序列的可线性化性。关键是:不论哪个先执行,只要一个抛出异常、另一个本可成功,都不会留下资源。若 driver 先执行而 user 抛出异常,driver 仍会退出,而用户没有创建;若 driver 抛出异常,浏览器从未启动,用户也从未创建。
安全地执行多个断言
有时完成全部准备后,希望执行多个断言;复杂系统中,一个动作可能触发多个行为。pytest 有一种方便的方法,把前面介绍的机制结合起来。
只需扩大作用域,把动作步骤定义成 autouse fixture,并确保所有 fixture 都使用这个更大的作用域。
修改前面的示例:除检查页头欢迎消息,还要检查退出登录按钮和用户资料链接。
下面看怎样组织结构,以便执行多个断言,而不重复所有准备步骤。
注意
此例假定某些 fixture,例如 base_url 与 admin_credentials,已在其他地方定义。这里暂且假定它们存在,只是不展示。
# contents of tests/end_to_end/test_login.py
from uuid import uuid4
from urllib.parse import urljoin
from selenium.webdriver import Chrome
import pytest
from src.utils.pages import LoginPage, LandingPage
from src.utils import AdminApiClient
from src.utils.data_types import User
@pytest.fixture(scope="class")
def admin_client(base_url, admin_credentials):
return AdminApiClient(base_url, **admin_credentials)
@pytest.fixture(scope="class")
def user(admin_client):
_user = User(name="Susan", username=f"testuser-{uuid4()}", password="P4$$word")
admin_client.create_user(_user)
yield _user
admin_client.delete_user(_user)
@pytest.fixture(scope="class")
def driver():
_driver = Chrome()
yield _driver
_driver.quit()
@pytest.fixture(scope="class")
def landing_page(driver, login):
return LandingPage(driver)
class TestLandingPageSuccess:
@pytest.fixture(scope="class", autouse=True)
@classmethod
def login(cls, driver, base_url, user):
driver.get(urljoin(base_url, "/login"))
page = LoginPage(driver)
page.login(user)
def test_name_in_header(self, landing_page, user):
assert landing_page.header == f"Welcome, {user.name}!"
def test_sign_out_button(self, landing_page):
assert landing_page.sign_out_button.is_displayed()
def test_profile_link(self, landing_page, user):
profile_href = urljoin(base_url, f"/profile?id={user.profile_id}")
assert landing_page.profile_link.get_attribute("href") == profile_href
注意,方法签名中的 self 仅用于形式要求;实际测试类没有像 unittest.TestCase 那样绑定状态。所有内容由 pytest fixture 系统管理。
每个方法只须请求自己实际需要的 fixture,而不用关心顺序,因为动作 fixture 是 autouse fixture,确保其他 fixture 都已先执行。此后没有更多状态变更,测试可以进行任意数量的不改变状态的查询,不必担心干扰其他测试。
login fixture 也定义在类内,因为模块中的其他测试未必预期登录成功;另一个测试类可能需要不同的动作。例如要测试错误凭据,可在文件中添加如下内容:
class TestLandingPageBadCredentials:
@pytest.fixture(scope="class")
@classmethod
def faux_user(cls, user):
_user = deepcopy(user)
_user.password = "badpass"
return _user
def test_raises_bad_credentials_exception(self, login_page, faux_user):
with pytest.raises(BadCredentialsException):
login_page.login(faux_user)
Fixture 可以内省请求测试的上下文
Fixture 函数可以接收 request 对象,检查请求它的测试函数、类或模块上下文。继续扩展 smtp_connection 示例,从使用 fixture 的测试模块中读取可选的服务器 URL:
# content of conftest.py
import smtplib
import pytest
@pytest.fixture(scope="module")
def smtp_connection(request):
server = getattr(request.module, "smtpserver", "smtp.gmail.com")
smtp_connection = smtplib.SMTP(server, 587, timeout=5)
yield smtp_connection
print(f"finalizing {smtp_connection} ({server})")
smtp_connection.close()
通过 request.module,从测试模块中尝试获取 smtpserver 属性。再次执行时,结果基本没变化:
$ pytest -s -q --tb=no test_module.py
FFfinalizing <smtplib.SMTP object at 0xdeadbeef0002> (smtp.gmail.com)
========================= short test summary info ==========================
FAILED test_module.py::test_ehlo - assert 0
FAILED test_module.py::test_noop - assert 0
2 failed in 0.12s
现在创建另一个测试模块,在其模块命名空间中设置服务器 URL:
# content of test_anothersmtp.py
smtpserver = "mail.python.org" # will be read by smtp fixture
def test_showhelo(smtp_connection):
assert 0, smtp_connection.helo()
执行它:
$ pytest -qq --tb=short test_anothersmtp.py
F [100%]
================================= FAILURES =================================
______________________________ test_showhelo _______________________________
test_anothersmtp.py:6: in test_showhelo
assert 0, smtp_connection.helo()
E AssertionError: (250, b'mail.python.org')
E assert 0
------------------------- Captured stdout teardown -------------------------
finalizing <smtplib.SMTP object at 0xdeadbeef0003> (mail.python.org)
========================= short test summary info ==========================
FAILED test_anothersmtp.py::test_showhelo - AssertionError: (250, b'mail....
成功!smtp_connection fixture 从模块命名空间中取得了邮件服务器名称。
使用标记向 fixture 传递数据
Fixture 也可以通过 request 对象访问测试函数上的标记,从而把测试中的数据传给 fixture:
import pytest
@pytest.fixture
def fixt(request):
marker = request.node.get_closest_marker("fixt_data")
if marker is None:
# Handle missing marker in some way...
data = None
else:
data = marker.args[0]
# Do something with the data
return data
@pytest.mark.fixt_data(42)
def test_fixt(fixt):
assert fixt == 42
把工厂用作 fixture
当同一测试多次需要 fixture 的结果时,工厂 fixture 模式很有用。Fixture 不直接返回数据,而是返回生成数据的函数,测试再多次调用它。
工厂可以按需要接收参数:
@pytest.fixture
def make_customer_record():
def _make_customer_record(name):
return {"name": name, "orders": []}
return _make_customer_record
def test_customer_records(make_customer_record):
customer_1 = make_customer_record("Lisa")
customer_2 = make_customer_record("Mike")
customer_3 = make_customer_record("Meredith")
如果工厂创建的数据需要资源管理,fixture 可以负责处理:
@pytest.fixture
def make_customer_record():
created_records = []
def _make_customer_record(name):
record = models.Customer(name=name, orders=[])
created_records.append(record)
return record
yield _make_customer_record
for record in created_records:
record.destroy()
def test_customer_records(make_customer_record):
customer_1 = make_customer_record("Lisa")
customer_2 = make_customer_record("Mike")
customer_3 = make_customer_record("Meredith")
参数化 fixture
Fixture 函数可以参数化,因而多次调用,每次运行依赖于它的那组测试。测试函数通常无需关心自己被再次运行。参数化有助于为支持多种配置的组件编写全面的功能测试。
扩展前面的示例,让 fixture 创建两个 smtp_connection 实例;所有使用它的测试都会运行两次。Fixture 通过特殊的 request 对象访问每个参数:
# content of conftest.py
import smtplib
import pytest
@pytest.fixture(scope="module", params=["smtp.gmail.com", "mail.python.org"])
def smtp_connection(request):
smtp_connection = smtplib.SMTP(request.param, 587, timeout=5)
yield smtp_connection
print(f"finalizing {smtp_connection}")
smtp_connection.close()
主要变化是在 @pytest.fixture 中声明 params:这是一个值列表,每个值都会使 fixture 执行一次,并可通过 request.param 访问。测试函数不用改动。再次运行如下:
$ pytest -q test_module.py
FFFF [100%]
================================= FAILURES =================================
________________________ test_ehlo[smtp.gmail.com] _________________________
smtp_connection = <smtplib.SMTP object at 0xdeadbeef0004>
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
assert b"smtp.gmail.com" in msg
> assert 0 # for demo purposes
^^^^^^^^
E assert 0
test_module.py:7: AssertionError
________________________ test_noop[smtp.gmail.com] _________________________
smtp_connection = <smtplib.SMTP object at 0xdeadbeef0004>
def test_noop(smtp_connection):
response, msg = smtp_connection.noop()
assert response == 250
> assert 0 # for demo purposes
^^^^^^^^
E assert 0
test_module.py:13: AssertionError
________________________ test_ehlo[mail.python.org] ________________________
smtp_connection = <smtplib.SMTP object at 0xdeadbeef0005>
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
> assert b"smtp.gmail.com" in msg
E AssertionError: assert b'smtp.gmail.com' in b'mail.python.org\nPIPELINING\nSIZE 51200000\nETRN\nSTARTTLS\nAUTH DIGEST-MD5 NTLM CRAM-MD5\nENHANCEDSTATUSCODES\n8BITMIME\nDSN\nSMTPUTF8\nCHUNKING'
test_module.py:6: AssertionError
-------------------------- Captured stdout setup ---------------------------
finalizing <smtplib.SMTP object at 0xdeadbeef0004>
________________________ test_noop[mail.python.org] ________________________
smtp_connection = <smtplib.SMTP object at 0xdeadbeef0005>
def test_noop(smtp_connection):
response, msg = smtp_connection.noop()
assert response == 250
> assert 0 # for demo purposes
^^^^^^^^
E assert 0
test_module.py:13: AssertionError
------------------------- Captured stdout teardown -------------------------
finalizing <smtplib.SMTP object at 0xdeadbeef0005>
========================= short test summary info ==========================
FAILED test_module.py::test_ehlo[smtp.gmail.com] - assert 0
FAILED test_module.py::test_noop[smtp.gmail.com] - assert 0
FAILED test_module.py::test_ehlo[mail.python.org] - AssertionError: asser...
FAILED test_module.py::test_noop[mail.python.org] - assert 0
4 failed in 0.12s
两个测试函数都运行了两次,分别使用不同的 smtp_connection 实例。还要注意 mail.python.org 连接对应的 test_ehlo 失败,因为预期服务器字符串与收到的不同。
pytest 为参数化 fixture 的每个值生成测试 ID,例如 test_ehlo[smtp.gmail.com] 与 test_ehlo[mail.python.org]。可用 -k 选择特定用例;发生失败时,ID 也会标明是哪一用例。执行 pytest –collect-only 可显示生成的 ID。
数字、字符串、布尔值和 None 使用通常的字符串表示作为测试 ID;其他对象则按参数名生成字符串。可以使用 ids 关键字参数,为特定 fixture 值定制 ID:
# content of test_ids.py
import pytest
@pytest.fixture(params=[0, 1], ids=["spam", "ham"])
def a(request):
return request.param
def test_a(a):
pass
def idfn(fixture_value):
if fixture_value == 0:
return "eggs"
else:
return None
@pytest.fixture(params=[0, 1], ids=idfn)
def b(request):
return request.param
def test_b(b):
pass
ids 可以是字符串列表,也可以是一个函数:后者接收 fixture 值,并返回用作 ID 的字符串。如果函数返回 None,就使用 pytest 自动生成的 ID。
运行上述测试,会使用以下测试 ID:
$ pytest --collect-only
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-9.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 12 items
<Dir fixtures.rst-236>
<Module test_anothersmtp.py>
<Function test_showhelo[smtp.gmail.com]>
<Function test_showhelo[mail.python.org]>
<Module test_emaillib.py>
<Function test_email_received>
<Module test_finalizers.py>
<Function test_bar>
<Module test_ids.py>
<Function test_a[spam]>
<Function test_a[ham]>
<Function test_b[eggs]>
<Function test_b[1]>
<Module test_module.py>
<Function test_ehlo[smtp.gmail.com]>
<Function test_noop[smtp.gmail.com]>
<Function test_ehlo[mail.python.org]>
<Function test_noop[mail.python.org]>
======================= 12 tests collected in 0.12s ========================
为参数化 fixture 使用标记
可以用 pytest.param() 给参数化 fixture 的值集合添加标记,方式与 @pytest.mark.parametrize 相同。
示例:
# content of test_fixture_marks.py
import pytest
@pytest.fixture(params=[0, 1, pytest.param(2, marks=pytest.mark.skip)])
def data_set(request):
return request.param
def test_data(data_set):
pass
运行该测试,会跳过 data_set 值为 2 的调用:
$ pytest test_fixture_marks.py -v
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-9.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python
cachedir: .pytest_cache
rootdir: /home/sweet/project
collecting ... collected 3 items
test_fixture_marks.py::test_data[0] PASSED [ 33%]
test_fixture_marks.py::test_data[1] PASSED [ 66%]
test_fixture_marks.py::test_data[2] SKIPPED (unconditional skip) [100%]
======================= 2 passed, 1 skipped in 0.12s =======================
模块化:在 fixture 函数中使用 fixture
除在测试函数中使用 fixture,fixture 函数也可以使用其他 fixture。这有助于模块化设计,并在多个项目中复用特定框架的 fixture。例如,扩展之前的例子,创建 app 对象,把已定义的 smtp_connection 资源放进去:
# content of test_appsetup.py
import pytest
class App:
def __init__(self, smtp_connection):
self.smtp_connection = smtp_connection
@pytest.fixture(scope="module")
def app(smtp_connection):
return App(smtp_connection)
def test_smtp_connection_exists(app):
assert app.smtp_connection
这里声明 app fixture,它接收前面定义的 smtp_connection,并用它创建 App 对象。运行如下:
$ pytest -v test_appsetup.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-9.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python
cachedir: .pytest_cache
rootdir: /home/sweet/project
collecting ... collected 2 items
test_appsetup.py::test_smtp_connection_exists[smtp.gmail.com] PASSED [ 50%]
test_appsetup.py::test_smtp_connection_exists[mail.python.org] PASSED [100%]
============================ 2 passed in 0.12s =============================
由于 smtp_connection 被参数化,测试运行两次,分别使用不同的 App 实例及 SMTP 服务器。app 不必知道 smtp_connection 的参数化,因为 pytest 会完整分析 fixture 依赖图。
注意,app 使用 module 作用域,也依赖 module 作用域的 smtp_connection。如果 smtp_connection 缓存在 session 作用域,此例仍能运行。Fixture 可以使用作用域更广的 fixture,反过来不行:会话级 fixture 无法有意义地使用模块级 fixture。
按 fixture 实例自动分组测试
pytest 会尽量减少运行测试时活跃的 fixture 数量。对于参数化 fixture,所有使用它的测试先以一个实例执行;随后调用 finalizer,再创建下一个实例。这也方便测试创建或使用全局状态的应用。
以下示例使用两个参数化 fixture,其中一个作用域是模块。所有函数都打印消息,以展示准备与清理流程:
# content of test_module.py
import pytest
@pytest.fixture(scope="module", params=["mod1", "mod2"])
def modarg(request):
param = request.param
print(" SETUP modarg", param)
yield param
print(" TEARDOWN modarg", param)
@pytest.fixture(scope="function", params=[1, 2])
def otherarg(request):
param = request.param
print(" SETUP otherarg", param)
yield param
print(" TEARDOWN otherarg", param)
def test_0(otherarg):
print(" RUN test0 with otherarg", otherarg)
def test_1(modarg):
print(" RUN test1 with modarg", modarg)
def test_2(otherarg, modarg):
print(f" RUN test2 with otherarg {otherarg} and modarg {modarg}")
以详细模式执行测试,并显示打印输出:
$ pytest -v -s test_module.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-9.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python
cachedir: .pytest_cache
rootdir: /home/sweet/project
collecting ... collected 8 items
test_module.py::test_0[1] SETUP otherarg 1
RUN test0 with otherarg 1
PASSED TEARDOWN otherarg 1
test_module.py::test_0[2] SETUP otherarg 2
RUN test0 with otherarg 2
PASSED TEARDOWN otherarg 2
test_module.py::test_1[mod1] SETUP modarg mod1
RUN test1 with modarg mod1
PASSED
test_module.py::test_2[mod1-1] SETUP otherarg 1
RUN test2 with otherarg 1 and modarg mod1
PASSED TEARDOWN otherarg 1
test_module.py::test_2[mod1-2] SETUP otherarg 2
RUN test2 with otherarg 2 and modarg mod1
PASSED TEARDOWN otherarg 2
test_module.py::test_1[mod2] TEARDOWN modarg mod1
SETUP modarg mod2
RUN test1 with modarg mod2
PASSED
test_module.py::test_2[mod2-1] SETUP otherarg 1
RUN test2 with otherarg 1 and modarg mod2
PASSED TEARDOWN otherarg 1
test_module.py::test_2[mod2-2] SETUP otherarg 2
RUN test2 with otherarg 2 and modarg mod2
PASSED TEARDOWN otherarg 2
TEARDOWN modarg mod2
============================ 8 passed in 0.12s =============================
可以看到,参数化模块级 modarg 资源使测试按尽量减少活跃资源的顺序执行。在准备 mod2 之前,先执行了 mod1 资源的 finalizer。
特别注意,完全独立的 test_0 先完成。随后依次执行使用 mod1 的 test_1 和 test_2,再执行使用 mod2 的 test_1 和 test_2。
另一个参数化资源 otherarg 使用 function 作用域,会在每个使用它的测试之前准备、之后清理。
通过 usefixtures 在类和模块中使用 fixture
有时测试不需要直接访问 fixture 对象。例如,测试需要以空目录作为当前工作目录,却不关心具体目录。可以结合标准库 tempfile 和 pytest fixture 实现,并把 fixture 的创建放进 conftest.py:
# content of conftest.py
import os
import tempfile
import pytest
@pytest.fixture
def cleandir():
with tempfile.TemporaryDirectory() as newpath:
old_cwd = os.getcwd()
os.chdir(newpath)
yield
os.chdir(old_cwd)
再用 usefixtures 标记在测试模块中声明使用它:
# content of test_setenv.py
import os
import pytest
@pytest.mark.usefixtures("cleandir")
class TestDirectoryInit:
def test_cwd_starts_empty(self):
assert os.listdir(os.getcwd()) == []
with open("myfile", "w", encoding="utf-8") as f:
f.write("hello")
def test_cwd_again_starts_empty(self):
assert os.listdir(os.getcwd()) == []
因为 usefixtures 标记,执行每个测试方法时都需要 cleandir,效果与为各方法添加 cleandir 参数相同。运行以确认 fixture 生效、测试通过:
$ pytest -q
.. [100%]
2 passed in 0.12s
可以这样指定多个 fixture:
@pytest.mark.usefixtures("cleandir", "anotherfixture")
def test(): ...
也可以通过 pytestmark 在测试模块级别指定:
pytestmark = pytest.mark.usefixtures("cleandir")
还可以把项目所有测试都需要的 fixture 写入配置文件:
# content of pytest.toml
[pytest]
usefixtures = ["cleandir"]
警告
@pytest.mark.usefixtures 不能用于 fixture 函数。如下用法是错误的:
@pytest.mark.usefixtures("my_other_fixture")
@pytest.fixture
def my_fixture_that_sadly_wont_use_my_other_fixture(): ...
在不同层级覆盖 fixture
较大的测试套件中,可能希望在某些测试模块或目录中覆盖 fixture,以扩展或改变其行为。
在目录层级(conftest)覆盖 fixture
假设测试文件结构如下:
tests/
conftest.py
# content of tests/conftest.py
import pytest
@pytest.fixture
def username():
return 'username'
test_something.py
# content of tests/test_something.py
def test_username(username):
assert username == 'username'
subdir/
conftest.py
# content of tests/subdir/conftest.py
import pytest
@pytest.fixture
def username(username):
return 'overridden-' + username
test_something_else.py
# content of tests/subdir/test_something_else.py
def test_username(username):
assert username == 'overridden-username'
如上所示,可以在特定测试目录层级覆盖同名 fixture。覆盖版本也能方便地访问基础或父级 fixture,本例就采用了这种方式。
在测试模块层级覆盖 fixture
假设测试文件结构如下:
tests/
conftest.py
# content of tests/conftest.py
import pytest
@pytest.fixture
def username():
return 'username'
test_something.py
# content of tests/test_something.py
import pytest
@pytest.fixture
def username(username):
return 'overridden-' + username
def test_username(username):
assert username == 'overridden-username'
test_something_else.py
# content of tests/test_something_else.py
import pytest
@pytest.fixture
def username(username):
return 'overridden-else-' + username
def test_username(username):
assert username == 'overridden-else-username'
上例在特定测试模块中覆盖了同名 fixture。
通过直接测试参数化覆盖 fixture
假设测试文件结构如下:
tests/
conftest.py
# content of tests/conftest.py
import pytest
@pytest.fixture
def username():
return 'username'
@pytest.fixture
def other_username(username):
return 'other-' + username
test_something.py
# content of tests/test_something.py
import pytest
@pytest.mark.parametrize('username', ['directly-overridden-username'])
def test_username(username):
assert username == 'directly-overridden-username'
@pytest.mark.parametrize('username', ['directly-overridden-username-other'])
def test_username_other(other_username):
assert other_username == 'other-directly-overridden-username-other'
上例用测试参数值覆盖 fixture 的值。即使测试未直接使用该 fixture,也就是函数签名中没有声明它,仍可以这样覆盖。
以非参数化 fixture 覆盖参数化 fixture,或反之
假设测试文件结构如下:
tests/
conftest.py
# content of tests/conftest.py
import pytest
@pytest.fixture(params=['one', 'two', 'three'])
def parametrized_username(request):
return request.param
@pytest.fixture
def non_parametrized_username(request):
return 'username'
test_something.py
# content of tests/test_something.py
import pytest
@pytest.fixture
def parametrized_username():
return 'overridden-username'
@pytest.fixture(params=['one', 'two', 'three'])
def non_parametrized_username(request):
return request.param
def test_username(parametrized_username):
assert parametrized_username == 'overridden-username'
def test_parametrized_username(non_parametrized_username):
assert non_parametrized_username in ['one', 'two', 'three']
test_something_else.py
# content of tests/test_something_else.py
def test_username(parametrized_username):
assert parametrized_username in ['one', 'two', 'three']
def test_username(non_parametrized_username):
assert non_parametrized_username == 'username'
上例在特定测试模块中,用非参数化版本覆盖参数化 fixture,也用参数化版本覆盖非参数化 fixture。这显然也适用于测试目录层级。
使用其他项目的 fixture
提供 pytest 支持的项目通常使用入口点,因此把它们安装到环境中,其 fixture 就可使用。
如果项目没有使用入口点,可在顶层 conftest.py 中定义 pytest_plugins,把该模块注册为插件。
假设 mylibrary.fixtures 中有一些 fixture,希望在 app/tests 目录中复用。
只需在 app/tests/conftest.py 中定义指向该模块的 pytest_plugins。
pytest_plugins = "mylibrary.fixtures"
这实际把 mylibrary.fixtures 注册为插件,让 app/tests 中的测试可以使用其全部 fixture 和 hook。
注意
有些用户直接导入其他项目的 fixture,但不推荐这样做:导入到模块中,会让 pytest 把这些 fixture 注册为定义在该模块内。
它可能产生轻微影响,例如在 pytest –help 中出现多次;更重要的是,这种行为可能在未来版本改变或失效,因此不推荐。











暂无评论内容