在使用 Pytest 进行自动化测试时,finalizer 函数扮演着至关重要的角色。它们负责在测试结束后进行清理工作,例如释放资源、关闭连接等。然而,finalizer 的执行顺序并非总是如我们所愿。本文将深入探讨 Pytest 中 finalizer 的 FILO(First In, Last Out,先进后出) 执行顺序,并通过实例分析常见的陷阱和解决方案。
问题场景重现:数据库连接泄露
假设我们有一个测试场景,需要连接数据库进行操作,并在测试结束后关闭连接。我们可能会这样编写测试代码:
import pytest
import pymysql
@pytest.fixture
def db_connection():
conn = pymysql.connect(host='localhost', user='root', password='password', database='testdb')
yield conn
print("Closing DB connection...") # 添加打印语句方便观察
conn.close()
@pytest.fixture
def db_cursor(db_connection):
cursor = db_connection.cursor()
yield cursor
print("Closing DB cursor...") # 添加打印语句方便观察
cursor.close()
def test_database_operation(db_cursor):
# 执行数据库操作
db_cursor.execute("SELECT 1")
result = db_cursor.fetchone()
assert result == (1,)
print("Test done...") # 添加打印语句方便观察
运行这个测试,我们期望的输出顺序是:
Test done...Closing DB cursor...Closing DB connection...
但实际运行结果却是:
Test done...Closing DB connection...Closing DB cursor...
这正是 finalizer 的 FILO 原则在起作用。db_connection fixture 先被调用,它的 finalizer 后执行。db_cursor fixture 后被调用,它的 finalizer 先执行。这可能导致资源未被正确释放,例如数据库连接泄露,进而影响后续测试。
底层原理深度剖析:Pytest 的 Fixture 管理
Pytest 使用一个栈(Stack)来管理 fixture 的 finalizer。当一个 fixture 被 yield 时,它的 finalizer 函数会被压入栈中。当测试结束时,Pytest 会按照后进先出的顺序(FILO)依次执行栈中的 finalizer 函数。
这种设计的原因是为了保证 fixture 之间的依赖关系能够正确处理。例如,如果 db_cursor 依赖于 db_connection,那么必须先关闭 db_cursor,才能安全地关闭 db_connection。
代码解决方案:调整 Fixture 顺序
要解决上述问题,最简单的方法是调整 fixture 的定义顺序,使得依赖关系反过来。
import pytest
import pymysql
@pytest.fixture
def db_cursor(db_connection):
conn = db_connection # 明确依赖
cursor = conn.cursor()
yield cursor
print("Closing DB cursor...")
cursor.close()
@pytest.fixture
def db_connection():
conn = pymysql.connect(host='localhost', user='root', password='password', database='testdb')
yield conn
print("Closing DB connection...")
conn.close()
def test_database_operation(db_cursor):
# 执行数据库操作
db_cursor.execute("SELECT 1")
result = db_cursor.fetchone()
assert result == (1,)
print("Test done...")
这种方式虽然简单,但可能会导致代码可读性下降。更好的方法是使用 yield 的返回值来传递资源。
配置解决方案:request.addfinalizer
Pytest 提供了 request.addfinalizer 方法,可以更灵活地控制 finalizer 的执行顺序。我们可以显式地将 finalizer 函数添加到 request 对象中,并指定其执行顺序。
import pytest
import pymysql
@pytest.fixture
def db_connection(request):
conn = pymysql.connect(host='localhost', user='root', password='password', database='testdb')
def fin():
print("Closing DB connection...")
conn.close()
request.addfinalizer(fin)
yield conn
@pytest.fixture
def db_cursor(db_connection, request):
cursor = db_connection.cursor()
def fin():
print("Closing DB cursor...")
cursor.close()
request.addfinalizer(fin)
yield cursor
def test_database_operation(db_cursor):
# 执行数据库操作
db_cursor.execute("SELECT 1")
result = db_cursor.fetchone()
assert result == (1,)
print("Test done...")
使用 request.addfinalizer 可以更清晰地表达资源清理的顺序,提高代码可读性。
实战避坑经验总结
- 理解 FILO 原则:牢记 Pytest finalizer 的 FILO 执行顺序,避免资源泄露等问题。
- 显式声明依赖:使用
yield的返回值或request.addfinalizer显式声明 fixture 之间的依赖关系,提高代码可读性和可维护性。 - 添加日志输出:在 finalizer 函数中添加日志输出,方便调试和排查问题。
- 关注性能影响:Finalizer 的执行也会消耗资源,在高并发场景下需要关注其对整体测试性能的影响。可以考虑使用缓存、连接池等技术来优化资源的使用。
掌握 pytest 的 finalizer 执行顺序对于编写健壮的自动化测试至关重要。理解 FILO 原则,并灵活运用 request.addfinalizer,可以有效地避免资源泄露等问题,提升测试的可靠性。在使用 Pytest 进行接口测试时,例如使用 requests 库发送 HTTP 请求,也需要注意及时关闭连接,释放资源,避免 Too many open files 错误。
冠军资讯
代码一只喵