pytest-2.3:fixture/funcarg 演进的原因¶
目标读者:阅读本文档需要具备基本的 Python 测试、xUnit 设置方法以及(之前的)基础 pytest funcarg 机制知识,请参阅 funcargs 和 pytest_funcarg__。如果您是 pytest 的新手,可以直接忽略此章节,阅读其他章节即可。
之前 pytest_funcarg__ 机制的缺点¶
pytest-2.3 之前的 funcarg 机制会在每次测试函数需要 funcarg 时调用一个工厂函数。如果工厂函数希望在不同的作用域之间重用资源,通常需要使用 request.cached_setup() 辅助工具来管理资源缓存。以下是如何实现基于会话(per-session)的数据库对象的基本示例
# content of conftest.py
class Database:
def __init__(self):
print("database instance created")
def destroy(self):
print("database instance destroyed")
def pytest_funcarg__db(request):
return request.cached_setup(
setup=DataBase, teardown=lambda db: db.destroy, scope="session"
)
这种方法存在几个局限性和困难
对 funcarg 资源创建进行作用域划分并不直观,必须理解复杂的 cached_setup() 方法机制。
对“db”资源进行参数化并不直观:你需要应用一个 “parametrize” 装饰器,或者实现一个
pytest_generate_tests钩子,并在资源被使用的地方调用parametrize()来执行参数化。此外,你需要修改工厂函数,向Request.cached_setup调用传递一个包含request.param的extrakey参数。多个参数化的会话级(session-scoped)资源会同时处于激活状态,这使得它们很难影响被测应用程序的全局状态。
无法在 xUnit 设置方法中使用 funcarg 工厂。
如果测试函数签名中没有声明,非参数化的 fixture 函数就无法使用参数化的 funcarg 资源。
pytest-2.3 及其改进的 fixture 机制解决了所有这些限制。
fixture/funcarg 工厂的直接作用域划分¶
你可以使用 @pytest.fixture 装饰器并直接声明作用域,而不是调用带有缓存作用域的 cached_setup()
@pytest.fixture(scope="session")
def db(request):
# factory will only be invoked once per session -
db = DataBase()
request.addfinalizer(db.destroy) # destroy when session is finished
return db
这个工厂实现不再需要调用 cached_setup(),因为它每个会话只会调用一次。此外,request.addfinalizer() 会根据工厂函数正在运行的指定资源作用域注册一个终结器(finalizer)。
funcarg 资源工厂的直接参数化¶
以前,funcarg 工厂不能直接进行参数化。你需要通过测试函数上的 @parametrize 装饰器,或者实现一个 pytest_generate_tests 钩子来执行参数化,即通过不同的值集多次调用同一个测试。pytest-2.3 引入了一个可以直接在工厂函数上使用的装饰器
@pytest.fixture(params=["mysql", "pg"])
def db(request): ... # use request.param
在这里,工厂函数将被调用两次(分别以 "mysql" 和 "pg" 作为 request.param 属性值),所有需要 "db" 的测试也将运行两次。"mysql" 和 "pg" 的值也将用于报告测试调用的变体。
这种参数化 funcarg 工厂的新方式在许多情况下允许重用已经写好的工厂函数,因为当测试函数/类通过 metafunc.parametrize(indirect=True) 调用进行参数化时,实际上就已经在使用 request.param 了。
当然,结合参数化和作用域划分是完全没问题的
@pytest.fixture(scope="session", params=["mysql", "pg"])
def db(request):
if request.param == "mysql":
db = MySQL()
elif request.param == "pg":
db = PG()
request.addfinalizer(db.destroy) # destroy when session is finished
return db
这将执行所有需要会话级 "db" 资源且包含两次参数化的测试,接收由工厂函数两次调用所创建的值。
使用 @fixture 装饰器时不再需要 pytest_funcarg__ 前缀¶
使用 @fixture 装饰器时,函数名即表示该资源可以作为函数参数被访问的名称
@pytest.fixture()
def db(request): ...
可以请求该 funcarg 资源的名称为 db。
你仍然可以使用定义 funcarg 工厂的“旧”非装饰器方式,即
def pytest_funcarg__db(request): ...
但这将无法定义作用域和参数化。因此,建议使用工厂装饰器。
解决会话级设置 / 自动使用(autouse)fixtures¶
长期以来,pytest 提供了 pytest_configure 和 pytest_sessionstart 钩子,常用于设置全局资源。这存在几个问题
在分布式测试中,管理进程会设置测试资源,但这些资源可能永远不会被用到,因为它只负责协调工作进程的测试活动。
如果你只执行收集(使用“–collect-only”),资源设置仍会被执行。
如果 pytest_sessionstart 包含在某个子目录的 conftest.py 文件中,它将不会被调用。这是因为该钩子实际上是用于报告的,特别是带有平台/自定义信息的测试头信息。
此外,除了实现一个 pytest_runtest_setup() 钩子并自行处理作用域/缓存外,很难从插件或 conftest 文件中定义一个具有作用域的设置。而且,由于 pytest_runtest_setup() 是在测试执行期间调用的,而参数化发生在收集期间,因此几乎不可能通过参数化来实现这一点。
由此可见,pytest_configure/session/runtest_setup 通常不适合实现通用的 fixture 需求。因此,pytest-2.3 引入了 自动使用 fixtures(无需请求的 fixture),它们与通用的 fixture 机制完全集成,并废弃了许多先前使用 pytest 钩子的方式。
funcargs/fixture 的发现现在发生在收集阶段¶
从 pytest-2.3 开始,fixture/funcarg 工厂的发现是在收集阶段(collection time)完成的。这对大型测试套件来说效率更高。此外,调用 “pytest –collect-only” 未来将能够显示大量的设置信息,从而成为获取项目中 fixture 管理概况的好方法。
结论和兼容性说明¶
funcargs 最初是在 pytest-2.0 中引入的。在 pytest-2.3 中,该机制得到了扩展和完善,现在被称为 fixtures。
以前,funcarg 工厂使用特殊的
pytest_funcarg__NAME前缀来指定,而不是使用@pytest.fixture装饰器。工厂函数接收一个
request对象,该对象通过request.cached_setup()调用管理缓存,并允许通过request.getfuncargvalue()调用使用其他 funcargs。这些复杂的 API 使得进行正确的参数化和实现资源缓存变得困难。新的pytest.fixture()装饰器允许声明作用域,并让 pytest 为你处理细节。如果你使用了参数化和使用了
request.cached_setup()的 funcarg 工厂,建议花几分钟时间简化你的 fixture 函数代码,改为使用 Fixtures 参考中的装饰器。这也将允许利用自动的按资源分组测试的功能。