pytest-factoryboy 中具有多 table 继承的 Django 模型的参数化属性

Parametrize attributes of Django-models with multi-table inheritance in pytest-factoryboy

我正在使用 Django 并想使用 pytestpytest-djangopytest-factoryboypytest-lazyfixtures.

编写测试

我有使用多table 继承的 Django 模型,如下所示:

class User(models.Model):
    created = models.DateTimeField()
    active = models.BooleanField()

class Editor(User):
    pass

class Admin(User):
   pass

我也为所有模型创建了工厂并注册了,如:

class UserFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = User

    created = ... # some datetime
    active = factory.Faker("pybool")

class EditorFactory(UserFactory):
    class Meta:
        model = Editor

...

现在我想测试一个函数,它可以将 UserEditorAdmin 中的任何一个作为输入,并使用所有用户类型和 [= 的变体对测试进行参数化23=] 和 created,就像这样(不幸的是它不能那样工作):


@pytest.mark.parametrize("any_user", [lazy_fixture("user"), lazy_fixture("editor"), lazy_fixture("admin")])
@pytest.mark.parametrize("any_user__active", [True, False])
def test_some_func(any_user):
   ...  # test some stuff

但是 In test_some_func: function uses no argument 'any_user__active' 失败了。

知道如何最好地解决这个问题吗?

我当然可以这样做,但不是很好:


@pytest.mark.parametrize("any_user", [lazy_fixture("user"), lazy_fixture("editor"), lazy_fixture("admin")])
@pytest.mark.parametrize("active", [True, False])
def test_some_func(any_user, active):
   any_user.active = active
   # save any_user if necessary
   ...  # test some stuff

有更好的建议吗?

However that fails with In test_some_func: function uses no argument 'any_user__active'.

这是因为您还没有将此 any_user__active 作为参数传递给测试函数。 所以将您的测试文件更改为

def test_some_func(any_user__active, any_user):

例子如下

@pytest.mark.parametrize("days, expected", [
        (-1, 0),
        (1, 1),
        (0, 0),
        (365, 365)
    ])
def test_subscription_to_for_user(days, expected):

在这种情况下,pytest-factoryboy 的表现力不如我希望的那样。用模型夹具的备用名称调用 pytest_factoryboy.register 会很好——但不幸的是,即使 register 采用用于此目的的 _name 参数,_name 也会被忽略,并使用 underscore(factory_class._meta.model.__name__) 代替。

谢天谢地,我们可以欺骗这个逻辑来使用我们想要的模型名称:

@register
class AnyUserFactory(UserFactory):
    class Meta:
        model = type('AnyUser', (User,), {})

本质上,我们创建了一个名为 AnyUserUser 的新子 class。这将导致 pytest-factoryboy 创建 any_user 模型夹具,以及 any_user__activeany_user__created 等。现在,我们如何参数化 any_user 以使用 UserFactoryEditorFactoryAdminFactory?

再次谢天谢地,模型夹具通过使用 request.getfixturevalue('model_name_factory') 请求 model_name_factory 夹具来工作,而不是通过直接引用 @register 的工厂 class。结果是我们可以简单地用我们想要的任何工厂覆盖 any_user_factory

@pytest.fixture(autouse=True, params=[
    lazy_fixture('user_factory'),
    lazy_fixture('editor_factory'),
    lazy_fixture('admin_factory'),
])
def any_user_factory(request):
    return request.param

注意:pytest 似乎会根据测试方法参数以及灯具请求的任何参数来修剪可用灯具的图形。当夹具使用 request.getfixturevalue 时,pytest 可能会报告无法找到请求的夹具——即使它已明确定义——因为它已被修剪。我们将 autouse=True 传递给我们的夹具,以强制 pytest 将其包含在依赖关系图中。

现在,我们可以直接在测试中参数化 any_user__activeany_user 将是每个值的 UserEditorAdmin active

@pytest.mark.parametrize('any_user__active', [True, False])
def test_some_func(any_user):
    print(f'{type(any_user)=} {any_user.active=}')

输出:

py.test test.py -sq

type(any_user)=<class 'test.User'> any_user.active=True
.type(any_user)=<class 'test.User'> any_user.active=False
.type(any_user)=<class 'test.Editor'> any_user.active=True
.type(any_user)=<class 'test.Editor'> any_user.active=False
.type(any_user)=<class 'test.Admin'> any_user.active=True
.type(any_user)=<class 'test.Admin'> any_user.active=False
.
6 passed in 0.04s

此外,如果 @pytest.fixturerequest.param 感觉有点冗长,我可能会建议使用 pytest-lambda免责声明: 我是作者).有时,@pytest.mark.parametrize 可能会受到限制,或者可能需要在未使用的测试方法中包含额外的参数名称;在这些情况下,无需编写完整的 fixture 方法就可以方便地声明新的 fixture。

from pytest_lambda import lambda_fixture

any_user_factory = lambda_fixture(autouse=True, params=[
    lazy_fixture('user_factory'),
    lazy_fixture('editor_factory'),
    lazy_fixture('admin_factory'),
])

@pytest.mark.parametrize('any_user__active', [True, False])
def test_some_func(any_user):
    print(f'{type(any_user)=} {any_user.active=}')

如果在 any_user_factory 上包含 autouse=True 很麻烦,因为它会导致所有其他测试被参数化,我们必须找到其他方法将 any_user_factory 包含在 pytest 依赖关系图中.

不幸的是,我尝试的第一种方法导致了错误。我试图覆盖 any_user 夹具,同时请求原始 any_user 夹具和我们覆盖的 any_user_factory,就像这样

@pytest.fixture
def any_user(any_user, any_user_factory):
    return any_user

唉,pytest 不喜欢那样

___________________________ ERROR collecting test.py ___________________________
In test_some_func: function uses no argument 'any_user__active'

幸运的是,pytest-lambda 提供了一个装饰器来包装 fixture 函数,因此被装饰的方法和包装的 fixture 的参数都被保留了下来。这允许我们显式地将 any_user_factory 添加到依赖关系图

from pytest_lambda import wrap_fixture

@pytest.fixture(params=[  # NOTE: no autouse
    lazy_fixture('user_factory'),
    lazy_fixture('editor_factory'),
    lazy_fixture('admin_factory'),
])
def any_user_factory(request):
    return request.param

@pytest.fixture
@wrap_fixture(any_user)
def any_user(any_user_factory, wrapped):
    return wrapped()  # calls the original any_user() fixture method

注意:@wrap_fixture(any_user)在调用@register时直接引用了pytest_factoryboy定义的any_userfixture方法。它会在大多数静态代码检查器/IDE 中显示为未解析的引用;但只要它出现在 class AnyUserFactory 之后并且在同一个模块中,它就可以工作。

现在,仅测试请求 any_user 将命中 any_user_factory 并接收其参数化。

@pytest.mark.parametrize('any_user__active', [True, False])
def test_some_func( any_user):
    print(f'{type(any_user)=} {any_user.active=}')

def test_some_other_func():
    print('some_other_func')

输出:

py.test test.py -sq

type(any_user)=<class 'test.User'> any_user.active=True
.type(any_user)=<class 'test.User'> any_user.active=False
.type(any_user)=<class 'test.Editor'> any_user.active=True
.type(any_user)=<class 'test.Editor'> any_user.active=False
.type(any_user)=<class 'test.Admin'> any_user.active=True
.type(any_user)=<class 'test.Admin'> any_user.active=False
.some_other_func
.
7 passed in 0.06 seconds