pytest-2.3: fixture/funcarg 演进的原因

目标读者:阅读本文档需要了解 Python 测试、xUnit 设置方法以及(以前的)基本 pytest funcarg 机制的基础知识,请参阅funcargs 和 pytest_funcarg__。如果您是 pytest 新手,则可以忽略此部分并阅读其他部分。

以前 pytest_funcarg__ 机制的缺点

pytest-2.3 之前的 funcarg 机制在每次需要测试函数的 funcarg 时都会调用工厂。如果工厂希望在不同的作用域中重用资源,它通常会使用 request.cached_setup() 助手来管理资源的缓存。这里有一个基本的示例,说明我们如何实现一个会话级别的数据库对象

# 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"
    )

这种方法有几个限制和困难

  1. 对 funcarg 资源创建进行作用域划分并不简单,相反,必须理解复杂的 cached_setup() 方法机制。

  2. 参数化“db”资源并不简单:您需要应用“parametrize”装饰器或实现一个 pytest_generate_tests 钩子来调用 parametrize(),它在资源使用的地方执行参数化。此外,您需要修改工厂以使用包含 request.paramextrakey 参数到 Request.cached_setup 调用。

  3. 多个参数化的会话作用域资源将同时处于活动状态,这使得它们难以影响被测应用程序的全局状态。

  4. 无法在 xUnit 设置方法中使用 funcarg 工厂。

  5. 如果非参数化的 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() 根据工厂函数所操作的指定资源作用域注册一个终结器。

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): ...

但是,这样就无法定义作用域和参数化。因此,建议使用工厂装饰器。

解决会话级设置/自动使用 fixture

pytest 长期以来提供了 pytest_configure 和 pytest_sessionstart 钩子,它们通常用于设置全局资源。这存在几个问题

  1. 在分布式测试中,管理进程会设置测试资源,但这些资源从未被需要,因为它只协调工作进程的测试运行活动。

  2. 如果您只执行收集(使用“–collect-only”),资源设置仍将执行。

  3. 如果某个子目录的 conftest.py 文件中包含 pytest_sessionstart,它将不会被调用。这是因为此钩子实际上用于报告,特别是带有平台/自定义信息的测试头。

此外,从插件或 conftest 文件定义作用域设置并不容易,除非实现 pytest_runtest_setup() 钩子并自行处理作用域/缓存。而且,使用参数化几乎不可能做到这一点,因为 pytest_runtest_setup() 在测试执行期间调用,而参数化发生在收集时。

因此,pytest_configure/session/runtest_setup 通常不适合实现常见的 fixture 需求。因此,pytest-2.3 引入了 自动使用 fixture(您无需请求的 fixture),它与通用 fixture 机制完全集成,并取代了许多以前的 pytest 钩子用法。

funcargs/fixture 的发现现在在收集时发生

自 pytest-2.3 起,fixture/funcarg 工厂的发现是在收集时处理的。这对于大型测试套件来说效率更高。此外,将来调用“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 参考装饰器。这还将允许利用按资源自动分组测试的功能。