在自动化测试中,我们常常需要处理一些重复操作——比如每次执行测试用例前都要登录系统。如果每个用例都重复执行登录,不仅会浪费时间,还可能影响测试效率。最近在优化一个收货地址测试场景时,我通过调整 pytest fixture 的作用域,轻松解决了“重复登录”的问题。今天就以这个实际案例为例,聊聊 pytest fixture 作用域的用法和最佳实践。
一、问题场景:为什么我的测试在重复登录?
先看一个真实场景:在测试“新增收货地址”功能时,我写了一个测试类 TestAddAddress
,包含 4 个测试用例(成功添加、姓名为空失败、电话为空失败、地址详情为空失败)。为了保证测试独立,我用 fixture 封装了登录逻辑,代码大概长这样:
import pytest
from keywords import Keywords
# 登录 fixture,作用域默认是 function
@pytest.fixture(scope="function")
def logged_in_keywords(base_url, logger):
# 初始化关键词对象
keywords = Keywords(base_url, logger)
# 执行登录
login_response = keywords.login("user123", "user123")
assert login_response.get("errno") == 0, "登录失败"
# 返回登录后的关键词对象(携带 token)
yield keywords
# 测试类
class TestAddAddress:
# 4 个参数化用例,测试不同场景
@pytest.mark.parametrize("case_id, request_data, expected_results", test_data)
def test_add_address(self, logged_in_keywords, logger, case_id, request_data, expected_results):
# 调用新增地址接口(依赖登录后的 token)
response = logged_in_keywords.add_address(**request_data)
# 断言结果
assert response.get("errno") == expected_results["errno"]
运行后发现,日志里竟然有 4 次登录记录!每个用例执行前都会重新登录一次:
2025-09-25 16:26:21 - [INFO] - [test_add_address[add_address_success]] - --------- 关键字: login [开始] ---------
2025-09-25 16:26:21 - [INFO] - [test_add_address[add_address_fail_null_name]] - --------- 关键字: login [开始] ---------
2025-09-25 16:26:22 - [INFO] - [test_add_address[add_address_fail_null_tel]] - --------- 关键字: login [开始] ---------
2025-09-25 16:26:22 - [INFO] - [test_add_address[add_address_fail_null_addressdetail]] - --------- 关键字: login [开始] ---------
这显然不合理:同一批测试用例属于同一个用户,完全可以共享一次登录状态。问题出在哪?—— 答案是 fixture 的作用域(scope)。
二、什么是 fixture 作用域?
pytest 的 fixture 作用域决定了 fixture 被执行的频率,即“在什么范围内,fixture 只需要初始化一次”。默认情况下,fixture 的作用域是 function
(每个测试用例执行一次),这也是上面例子中“4 个用例执行 4 次登录”的原因。
pytest 提供了 5 种作用域,从小到大依次是:
三、作用域实战:从 4 次登录到 1 次登录
回到刚才的问题:4 个用例属于同一个测试类 TestAddAddress
,且共享同一个用户的登录状态。此时最适合的作用域是 class
——让 fixture 在整个测试类执行前只登录一次。
优化步骤:修改 fixture 作用域为 class
只需将 fixture 的 scope
参数从默认的 function
改为 class
:
# 优化后的 fixture:作用域改为 class
@pytest.fixture(scope="class") # 关键修改
def logged_in_keywords(base_url, logger):
keywords = Keywords(base_url, logger)
login_response = keywords.login("user123", "user123")
assert login_response.get("errno") == 0, "登录失败"
yield keywords # 整个类的用例共享此对象
再次运行测试,日志中只有 1 次登录 了:
2025-09-25 17:00:01 - [INFO] - [TestAddAddress] - --------- 关键字: login [开始] ---------
# 后续 4 个用例直接使用登录后的状态,不再重复登录
这是因为 scope="class"
让 fixture 在 TestAddAddress
类初始化时执行一次,生成的 keywords
对象(包含登录后的 token)会被类中所有用例共享,避免了重复登录。
四、不同作用域的适用场景
理解作用域的核心是:根据“资源的共享范围”选择合适的作用域。以下是各作用域的典型适用场景:
1. function
:每个用例独立初始化
适用场景:需要隔离的资源(如每个用例需要全新的测试数据、独立的数据库连接)。
例子:测试“用户注册”功能,每个用例需要不同的手机号,此时注册前的“清空用户数据”操作适合用 function
作用域,确保用例间不干扰。
@pytest.fixture(scope="function")
def clean_user_data(logger):
# 每个用例执行前清空测试用户数据
logger.info("清空用户数据")
db.execute("DELETE FROM user WHERE username LIKE 'test_%'")
yield
2. class
:同一类用例共享资源
适用场景:同一类用例属于同一功能模块,可共享前置条件(如登录状态、基础配置)。
例子:本文中的“新增收货地址”测试类,所有用例都依赖同一用户的登录状态,用 class
作用域最合理。
3. module
:模块内共享资源
适用场景:一个模块(.py 文件)内的多个测试类/函数共享资源(如数据库连接、接口基础配置)。
例子:一个 test_order.py
模块包含订单创建、支付、取消等测试类,可共享“初始化订单表”的 fixture:
@pytest.fixture(scope="module")
def init_order_table():
# 模块执行前初始化订单表(创建测试数据)
db.execute("INSERT INTO order (id, status) VALUES (1001, 'UNPAID')")
yield
# 模块执行后清理
db.execute("DELETE FROM order WHERE id=1001")
4. session
:全局共享资源
适用场景:整个测试过程中只需要初始化一次的资源(如启动测试服务、连接数据库)。
例子:UI 自动化测试中,浏览器驱动的初始化适合用 session
作用域,避免每次用例都启动新浏览器:
@pytest.fixture(scope="session")
def browser():
# 整个测试会话只启动一次浏览器
driver = webdriver.Chrome()
yield driver
# 会话结束后关闭浏览器
driver.quit()
五、作用域使用注意事项
作用域的“继承”与“限制”
如果 fixture A 依赖 fixture B,A 的作用域不能比 B 小。例如:class
级的 fixture 不能依赖function
级的 fixture(否则会导致 B 被重复执行)。共享资源的“污染”风险
作用域越大,资源被共享的范围越广,需注意用例对资源的“修改”是否会影响其他用例。例如:session
级的登录状态如果被某用例登出,会导致后续用例失败。优先选择“最小必要”作用域
不要盲目使用大作用域(如session
),应根据实际需求选择最小可行的作用域。例如:只有当所有用例都能共享同一资源时,才用session
。
六、总结
fixture 作用域是 pytest 中提升测试效率的关键特性。通过合理设置作用域,我们可以避免重复操作(如多次登录),同时保证测试的独立性和稳定性。
回到开头的例子,仅仅修改一个参数 scope="class"
,就将登录次数从 4 次减少到 1 次,测试效率提升明显。希望通过这个实际案例,你能更清晰地理解 fixture 作用域的用法——记住:作用域的本质是“资源共享的范围”,选择合适的范围,让测试既高效又可靠。