Параметризовать атрибуты Django -моделей с многостольным наследованием в pytest-factoryboy - PullRequest
2 голосов
/ 22 февраля 2020

Я использую Django и хочу написать тест, используя pytest, pytest-django, pytest-factoryboy и pytest-lazyfixtures.

У меня есть 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

...

Теперь я хочу протестировать функцию, которая может принимать любые из User, Editor или Admin в качестве входных данных и параметризация теста со всеми типами пользователей и вариациями active и 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

Есть лучшие предложения?

Ответы [ 2 ]

1 голос
/ 01 марта 2020

pytest-factoryboy не так выразителен, как я бы sh в подобных случаях. Было бы неплохо вызвать pytest_factoryboy.register с альтернативным именем для осветителей модели - но, к сожалению, даже если register принимает параметр _name, предназначенный для этой цели, _name игнорируется и вместо него используется underscore(factory_class._meta.model.__name__).

К счастью, мы можем обмануть эту логику c, используя желаемое название модели:

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

По сути, мы создаем новый подкласс User с именем AnyUser , Это приведет к тому, что pytest-factoryboy создаст модель прибора any_user вместе с any_user__active, any_user__created, et c. Теперь, как мы можем параметризовать any_user для использования UserFactory, EditorFactory и AdminFactory?

К счастью, модели снова работают, запрашивая устройство model_name_factory с request.getfixturevalue('model_name_factory'), а не путем прямой ссылки на класс фабрики @register. В результате мы можем просто переопределить any_user_factory с любой фабрикой, с которой мы будем sh!

@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__active непосредственно в нашем тесте, и any_user будет User, Editor и Admin с каждым значением 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.fixture с request.param кажется немного многословным, я мог бы предложить использовать pytest-lambda ( заявление об отказе: Я автор). Иногда @pytest.mark.parametrize может быть ограничивающим или требовать включения дополнительных имен аргументов в методе теста, который go не используется; в этих случаях может быть удобно объявить новые приборы без написания метода полного приспособления.

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=}')

Если включение autouse=True в any_user_factory является утомительным, потому что это вызывает все другие тесты чтобы быть параметризованным, мы должны найти какой-то другой способ включить 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 предоставляет декоратор для обёртывания функции фикстуры, поэтому аргументы как декорированного метода, так и обёрнутого фиксатора сохраняются. Это позволяет нам явно добавлять 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) напрямую ссылается на метод фикстуры any_user, определенный pytest_factoryboy при вызове @register. Он будет отображаться как неразрешенная ссылка в большинстве stati c проверок кода / 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
0 голосов
/ 25 февраля 2020

Однако это не работает с In test_some_fun c: функция не использует аргумент '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):
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...