перевод статьи "How to Provide Test Fixtures for Django Models in Pytest"

Если вы работаете в Django, фикстуры pytest могут помочь вам создавать тесты для ваших моделей, которые несложно поддерживать. Написание хороших тестов - решающий шаг в поддержании успешного приложения, а приспособления - ключевой ингредиент в обеспечении эффективности и действенности вашего набора тестов.

Фикстуры - это небольшие фрагменты данных, которые служат базой для ваших тестов.
По мере изменения ваших тестовых сценариев добавление, изменение и обслуживание ваших фикстур может стать проблемой. Но не волнуйтесь. Из этого обучения вы узнаете, как использовать плагин pytest-django, чтобы упростить написание новых тестовых случаев и фикстур.
В этом руководстве вы узнаете:
• Как создавать и загружать тестовые приложения в Django
• Как создавать и загружать фикстуры pytest для моделей Django
• Как использовать фабрики для создания тестовых приборов для моделей Django в pytest
• Как создать зависимости между тестовыми приборами, используя фабрику в качестве шаблона приспособлений.
Концепции, описанные в этом руководстве, подходят для любого проекта Python, использующего pytest. Для удобства в примерах используется Django ORM, но результаты могут быть воспроизведены и в других типах ORM и даже в проектах, которые не используют ORM или базы данных.
Для начала вы создадите новый проект Django. В этом руководстве вы напишете несколько тестов с использованием встроенного модуля аутентификации.
Настройка виртуальной среды Python
При создании нового проекта лучше всего также создать для него виртуальную среду. Виртуальная среда позволяет изолировать проект от других проектов на вашем компьютере. Таким образом, разные проекты могут использовать разные версии Python, Django или любого другого пакета, не мешая друг другу.

Вот как вы можете создать виртуальную среду в новом каталоге:

$ mkdir django_fixtures
$ cd django_fixtures
django_fixtures $ python -m venv venv

Выполнение этой команды создаст новый каталог с именем venv. В этом каталоге будут храниться все пакеты, которые вы устанавливаете в виртуальной среде.

Настройка проекта Django
Теперь, когда у вас есть новая виртуальная среда, пора создать проект Django. В вашем терминале активируйте виртуальную среду и установите Django:

$ source venv/bin/activate
$ pip install django

Теперь, когда у вас установлен Django, вы можете создать новый проект Django с именем
django_fixtures:

$ django-admin startproject django_fixtures

После выполнения этой команды вы увидите, что Django создал новые файлы и каталоги. Дополнительные сведения о том, как начать новый проект Django, см. В разделе Запуск проекта Django.

Чтобы завершить настройку проекта Django, примените миграции для встроенных модулей:

$ cd django_fixtures
$ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying admin.0003_logentry_add_action_flag_choices... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying auth.0010_alter_group_name_max_length... OK
Applying auth.0011_update_proxy_permissions... OK
Applying sessions.0001_initial... OK

В выходных данных перечислены все миграции, примененные Django. При запуске нового проекта Django применяет миграции для встроенных приложений, таких как auth, sessions и admin.

Теперь вы готовы приступить к написанию тестов и фикстур!

Создание фикстур Django
Django предоставляет собственный способ создания и загрузки фикстур для моделей из файлов. Файлы фикстур Django могут быть написаны как в JSON, так и в YAML. В этом руководстве вы будете работать с форматом JSON.

Самый простой способ создать фикстуру Django - использовать существующий объект. Запустите оболочку Django:

$ python manage.py shell
Python 3.8.0 (default, Oct 23 2019, 18:51:26)
[GCC 9.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
Внутри оболочки Django создайте новую группу с именем appusers:
>>> from django.contrib.auth.models import Group
>>> group = Group.objects.create(name="appusers")
>>> group.pk

Выйдите из оболочки Django с помощью exit () и выполните следующую команду из вашего терминала:
$ python manage.py dumpdata auth.Group --pk 1 --indent 4 > group.json
В этом примере вы используете команду dumpdata для создания файлов фикстур из существующих экземпляров модели. Давайте разберемся:

auth.Group: описывает, какую модель следует сбрасывать. Формат: . .

--pk 1: описывает, какой объект нужно сбросить. Значение представляет собой список первичных ключей, разделенных запятыми, например 1,2,3.

--indent 4: это необязательный аргумент форматирования, который сообщает Django, сколько пробелов нужно добавить перед каждым уровнем отступа в сгенерированном файле. Использование отступов делает файл фикстуры более читабельным.

> group.json: описывает, куда записывать вывод команды. В этом случае вывод будет записан в файл с именем group.json.

[
{
"model": "auth.group",
"pk": 1,
"fields": {
"name": "appusers",
"permissions": []
}
}
]

Файл фикстуры содержит список объектов. В этом случае у вас в списке только один объект. Каждый объект включает заголовок с названием модели и первичным ключом, а также словарь со значением для каждого поля в модели. Вы можете видеть, что прибор содержит имя группы appusers.

Вы можете создавать и редактировать файлы фикстуры вручную, но обычно удобнее создать объект заранее и использовать команду Django dumpdata для создания файла фикстуры.
Загрузка фикстур Django
Теперь, когда у вас есть файл фикстур, вы хотите загрузить его в базу данных. Но прежде чем вы это сделаете, вы должны открыть оболочку Django и удалить группу, которую вы уже создали:

>>> from django.contrib.auth.models import Group
>>> Group.objects.filter(pk=1).delete()
(1, {'auth.Group_permissions': 0, 'auth.User_groups': 0, 'auth.Group': 1})

Теперь, когда группа удалена, загрузите прибор с помощью команды loaddata:

$ python manage.py loaddata group.json
Installed 1 object(s) from 1 fixture(s)

Чтобы убедиться, что новая группа загружена, откройте оболочку Django и загрузите ее:

>>> from django.contrib.auth.models import Group
>>> group = Group.objects.get(pk=1)
>>> vars(group)
{'_state': ,
'id': 1,
'name': 'appusers'}

Отлично! Группа загрузилась. Вы только что создали и загрузили свой первый прибор Django.

Загрузка фикстур Django в тестах
Итак, вы создали и загрузили файл фикстуры из командной строки. Как теперь можно использовать его для тестирования? Чтобы увидеть, как фикстуры используются в тестах Django, создайте новый файл с именем test.py и добавьте следующий тест:

from django.test import TestCase
from django.contrib.auth.models import Group

class MyTest(TestCase):
def test_should_create_group(self):
group = Group.objects.get(pk=1)
self.assertEqual(group.name, "appusers")

Тест выбирает группу с первичным ключом 1 и проверяет, что ее имя - appusers.

Запустите тест со своего устройства:

$ python manage.py test test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
E
======================================================================
ERROR: test_should_create_group (test.MyTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/django_fixtures/django_fixtures/test.py", line 9, in test_should_create_group
group = Group.objects.get(pk=1)
File "/django_fixtures/venv/lib/python3.8/site-packages/django/db/models/manager.py", line 82, in manager_method
return getattr(self.get_queryset(), name)(*args, **kwargs)
File "/django_fixtures/venv/lib/python3.8/site-packages/django/db/models/query.py", line 415, in get
raise self.model.DoesNotExist(
django.contrib.auth.models.Group.DoesNotExist: Group matching query does not exist.

----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (errors=1)
Destroying test database for alias 'default'...

Тест не прошел, потому что группы с первичным ключом 1 не существует.

Чтобы загрузить фикстуру в тест, вы можете использовать специальный атрибут класса TestCase, называемый фикстурами:
from django.test import TestCase
from django.contrib.auth.models import Group

class MyTest(TestCase):
fixtures = ["group.json"]

def test_should_create_group(self):
group = Group.objects.get(pk=1)
self.assertEqual(group.name, "appusers")

Добавление этого атрибута в TestCase указывает Django загружать фикстуры перед выполнением каждого теста. Обратите внимание, что фикстуры принимают массив, поэтому вы можете предоставить несколько файлов фикстур для загрузки перед каждым тестом.

Теперь запуск теста дает следующий результат:
$ python manage.py test test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.005s

OK
Destroying test database for alias 'default'...
Превосходно! Группа загрузилась и тест прошел. Теперь вы можете использовать групповые пользователи приложений в своих тестах.

Ссылки на связанные объекты в фикстурах Django
До сих пор вы использовали только один файл с одним объектом. Однако в большинстве случаев в вашем приложении будет много моделей, и вам понадобится более одной модели для тестирования.

Чтобы увидеть, как зависимости между объектами выглядят в фикстурах Django, создайте новый экземпляр пользователя, а затем добавьте его в группу appusers, которую вы создали ранее:

>>> from django.contrib.auth.models import User, Group
>>> appusers = Group.objects.get(name="appusers")
>>> haki = User.objects.create_user("haki")
>>> haki.pk
1
>>> haki.groups.add(appusers)

Пользователь haki теперь является членом группы appusers. Чтобы увидеть, как выглядит прибор с внешним ключом, сгенерируйте прибор для пользователя 1:

$ python manage.py dumpdata auth.User --pk 1 --indent 4
[
{
"model": "auth.user",
"pk": 1,
"fields": {
"password": "!M4dygH3ZWfd0214U59OR9nlwsRJ94HUZtvQciG8y",
"last_login": null,
"is_superuser": false,
"username": "haki",
"first_name": "",
"last_name": "",
"email": "",
"is_staff": false,
"is_active": true,
"date_joined": "2019-12-07T09:32:50.998Z",
"groups": [
1
],
"user_permissions": []
}
}
]

Конструкция приспособления аналогична той, которую вы видели ранее.

Пользователь может быть связан с несколькими группами, поэтому группа полей содержит идентификаторы для всех групп, к которым принадлежит пользователь. В этом случае пользователь принадлежит к группе с первичным ключом 1, которая является вашей группой пользователей приложений.

Использование первичных ключей для ссылки на объекты в приборах не всегда является хорошей идеей. Первичный ключ группы - это произвольный идентификатор, который база данных присваивает группе при ее создании. В другой среде или на другом компьютере группа пользователей приложений может иметь другой идентификатор, и это не повлияет на объект.

Чтобы избежать использования произвольных идентификаторов, Django определяет концепцию естественных ключей. Естественный ключ - это уникальный идентификатор объекта, который не обязательно является первичным ключом. В случае групп две группы не могут иметь одно и то же имя, поэтому естественным ключом для группы может быть ее имя.

Чтобы использовать естественные ключи вместо первичных ключей для ссылки на связанные объекты в приспособлении Django, добавьте флаг --natural-foreign к команде dumpdata:

$ python manage.py dumpdata auth.User --pk 1 --indent 4 --natural-foreign
[
{
"model": "auth.user",
"pk": 1,
"fields": {
"password": "!f4dygH3ZWfd0214X59OR9ndwsRJ94HUZ6vQciG8y",
"last_login": null,
"is_superuser": false,
"username": "haki",
"first_name": "",
"last_name": "",
"email": "benita",
"is_staff": false,
"is_active": true,
"date_joined": "2019-12-07T09:32:50.998Z",
"groups": [
[
`appusers`
]
],
"user_permissions": []
}
}
]

Django сгенерировал фикстуру для пользователя, но вместо использования первичного ключа группы appusers он использовал имя группы.

Вы также можете добавить флаг --natural-primary, чтобы исключить первичный ключ объекта из фикстуры. Когда pk имеет значение null, первичный ключ будет установлен во время выполнения, обычно базой данных.

Поддержка фикстур Django
Крепления Django великолепны, но они также создают некоторые проблемы:

Обновление фикстур: фикстуры Django должны содержать все обязательные поля модели. Если вы добавляете новое поле, которое не допускает значения NULL, вы должны обновить фикстуры. В противном случае они не загрузятся. Обновление фикстур Django может стать обузой, если у вас их много.

Поддержание зависимостей между фикстурами: фикстуры Django, которые зависят от других фикстур, должны загружаться вместе и в определенном порядке. Не отставать от приспособлений по мере добавления новых тестовых примеров и изменения старых тестовых примеров может быть непросто.

По этим причинам фикстуры Django не идеальный выбор для часто меняющихся моделей. Например, было бы очень сложно поддерживать фикстуры Django для моделей, которые используются для представления основных объектов в приложении, таких как продажи, заказы, транзакции или резервирования.

С другой стороны, фикстуры Django - отличный вариант для следующих случаев использования:

Постоянные данные: это относится к моделям, которые редко меняются, например к кодам стран и почтовым индексам.

Исходные данные: это относится к моделям, в которых хранятся данные поиска вашего приложения, такие как категории продуктов, группы пользователей и типы пользователей.

Приспособления pytest в Django
В предыдущем разделе вы использовали встроенные инструменты Django для создания и загрузки фикстур. Приспособления, предоставляемые Django, отлично подходят для некоторых случаев использования, но не идеальны для других.

В этом разделе вы собираетесь поэкспериментировать с совсем другим типом прибора: прибором pytest. pytest предоставляет очень обширную систему приспособлений, которую вы можете использовать для создания надежного и поддерживаемого набора тестов.

Настройка pytest для проекта Django
Чтобы начать работу с pytest, вам сначала необходимо установить pytest и плагин Django для pytest. Выполните следующие команды в своем терминале, пока виртуальная среда активирована:

$ pip install pytest
$ pip install pytest-django

Плагин pytest-django поддерживается командой разработчиков pytest. Он предоставляет полезные инструменты для написания тестов для проектов Django с использованием pytest.

Затем вам нужно сообщить pytest, где он может найти настройки вашего проекта Django.
[pytest]
DJANGO_SETTINGS_MODULE=django_fixtures.settings
Это минимальный объем конфигурации, необходимый для работы pytest с вашим проектом Django. Есть еще много вариантов конфигурации, но этого достаточно для начала.

Наконец, чтобы проверить свою настройку, замените содержимое test.py этим фиктивным тестом:

def test_foo():
assert True
Чтобы запустить фиктивный тест, используйте команду pytest в своем терминале:
$ pytest test.py
============================== test session starts ======================
platform linux -- Python 3.7.4, pytest-5.2.0, py-1.8.0, pluggy-0.13.0
Django settings: django_fixtures.settings (from ini file)
rootdir: /django_fixtures, inifile: pytest.ini
plugins: django-3.5.1

Вы только что завершили настройку нового проекта Django с помощью pytest! Теперь вы готовы копать глубже.

Дополнительные сведения о настройке pytest и написании тестов см. В статье «Разработка через тестирование с помощью pytest».

Доступ к базе данных из тестов
В этом разделе вы собираетесь писать тесты, используя встроенный модуль аутентификации django.contrib.auth. Наиболее знакомые модели в этом модуле - Пользователь и Группа.

Чтобы начать работу с Django и pytest, напишите тест, чтобы проверить, правильно ли функция create_user (), предоставляемая Django, устанавливает имя пользователя:

from django.contrib.auth.models import User

def test_should_create_user_with_username() -> None:
user = User.objects.create_user("Haki")
assert user.username == "Haki"
Теперь попробуйте выполнить тест из вашей команды, например:
$ pytest test.py
================================== test session starts ===============
platform linux -- Python 3.7.4, pytest-5.2.0, py-1.8.0, pluggy-0.13.0
Django settings: django_fixtures.settings (from ini file)
rootdir: /django-django_fixtures/django_fixtures, inifile: pytest.ini
plugins: django-3.5.1
collected 1 item

test.py F

=============================== FAILURES =============================
____________________test_should_create_user_with_username ____________

def test_should_create_user_with_username() -> None:
> user = User.objects.create_user("Haki")

self = , name = None

def _cursor(self, name=None):
> self.ensure_connection()

E Failed: Database access not allowed, use the "django_db" mark, or the "db"
or "transactional_db" fixtures to enable it.

Команда не выполнена, и тест не выполнен. Сообщение об ошибке дает вам некоторую полезную информацию: Чтобы получить доступ к базе данных в тесте, вам необходимо внедрить специальный инструмент, называемый db. Приспособление db является частью плагина django-pytest, который вы установили ранее, и оно требуется для доступа к базе данных в тестах.

Внесите в тест приспособление db:

from django.contrib.auth.models import User

def test_should_create_user_with_username(db) -> None:
user = User.objects.create_user("Haki")
assert user.username == "Haki"
Запустите тест снова:
$ pytest test.py
================================== test session starts ===============
platform linux -- Python 3.7.4, pytest-5.2.0, py-1.8.0, pluggy-0.13.0
Django settings: django_fixtures.settings (from ini file)
rootdir: /django_fixtures, inifile: pytest.ini
plugins: django-3.5.1
collected 1 item

test.py .

Отлично! Команда выполнена успешно, и ваш тест пройден. Теперь вы знаете, как получить доступ к базе данных в тестах. Вы также добавили приспособление в тестовый пример по пути.

Создание фикстур для моделей Django
Теперь, когда вы знакомы с Django и pytest, напишите тест, чтобы проверить, что пароль, установленный с помощью set_password (), подтвержден должным образом. Замените содержимое test.py этим тестом:

from django.contrib.auth.models import User

def test_should_check_password(db) -> None:
user = User.objects.create_user("A")
user.set_password("secret")
assert user.check_password("secret") is True

def test_should_not_check_unusable_password(db) -> None:
user = User.objects.create_user("A")
user.set_password("secret")
user.set_unusable_password()
assert user.check_password("secret") is False

Первый тест проверяет, проверяется ли Django пользователь с пригодным паролем. Второй тест проверяет крайний случай, когда пароль пользователя непригоден и не должен проверяться Django.

Здесь следует сделать важное различие: приведенные выше тестовые примеры не проверяют create_user (). Они проверяют set_password (). Это означает, что изменение create_user () не должно влиять на эти тестовые примеры.

Также обратите внимание, что экземпляр User создается дважды, по одному разу для каждого тестового примера. В большом проекте может быть много тестов, для которых требуется экземпляр User. Если каждый тестовый пример будет создавать своего собственного пользователя, у вас могут возникнуть проблемы в будущем, если модель User изменится.

Чтобы повторно использовать объект во многих тестовых случаях, вы можете создать тестовое приспособление:
import pytest
from django.contrib.auth.models import User

@pytest.fixture
def user_A(db) -> User:
return User.objects.create_user("A")

def test_should_check_password(db, user_A: User) -> None:
user_A.set_password("secret")
assert user_A.check_password("secret") is True

def test_should_not_check_unusable_password(db, user_A: User) -> None:
user_A.set_password("secret")
user_A.set_unusable_password()
assert user_A.check_password("secret") is False

В приведенном выше коде вы создали функцию с именем user_A (), которая создает и возвращает новый экземпляр User. Чтобы пометить функцию как приспособление, вы украсили ее декоратором pytest.fixture. Как только функция помечена как приспособление, ее можно внедрить в тестовые примеры. В этом случае вы внедрили прибор user_A в два тестовых примера.

Обслуживание приспособлений при изменении требований
Допустим, вы добавили новое требование к своему приложению, и теперь каждый пользователь должен принадлежать к специальной группе app_user. Пользователи в этой группе могут просматривать и обновлять свои личные данные. Чтобы протестировать приложение, вам нужно, чтобы ваши тестовые пользователи также принадлежали к группе app_user:
import pytest
from django.contrib.auth.models import User, Group, Permission

@pytest.fixture
def user_A(db) -> Group:
group = Group.objects.create(name="app_user")
change_user_permissions = Permission.objects.filter(
codename__in=["change_user", "view_user"],
)
group.permissions.add(*change_user_permissions)
user = User.objects.create_user("A")
user.groups.add(group)
return user

def test_should_create_user(user_A: User) -> None:
assert user_A.username == "A"

def test_user_is_in_app_user_group(user_A: User) -> None:
assert user_A.groups.filter(name="app_user").exists()

Внутри фикстуры вы создали группу «app_user» и добавили к ней соответствующие разрешения change_user и view_user. Затем вы создали тестового пользователя и добавили его в группу «app_user».

Раньше вам нужно было просмотреть каждый тестовый пример, в котором был создан пользователь, и добавить его в группу. Используя приспособления, вы смогли внести изменения только один раз. После того, как вы изменили прибор, одно и то же изменение появилось в каждом тестовом примере, в который вы вводили user_A. Используя фикстуры, вы можете избежать повторения и сделать ваши тесты более удобными в сопровождении.

Вставка приспособлений в другие приспособления
У больших приложений обычно больше одного пользователя, и часто бывает необходимо протестировать их с несколькими пользователями. В этой ситуации вы можете добавить еще один прибор для создания тестового user_B:
import pytest
from django.contrib.auth.models import User, Group, Permission

@pytest.fixture
def user_A(db) -> User:
group = Group.objects.create(name="app_user")
change_user_permissions = Permission.objects.filter(
codename__in=["change_user", "view_user"],
)
group.permissions.add(*change_user_permissions)
user = User.objects.create_user("A")
user.groups.add(group)
return user

@pytest.fixture
def user_B(db) -> User:
group = Group.objects.create(name="app_user")
change_user_permissions = Permission.objects.filter(
codename__in=["change_user", "view_user"],
)
group.permissions.add(*change_user_permissions)
user = User.objects.create_user("B")
user.groups.add(group)
return user

def test_should_create_two_users(user_A: User, user_B: User) -> None:
assert user_A.pk != user_B.pk

Попробуйте запустить тест:
$ pytest test.py
==================== test session starts =================================
platform linux -- Python 3.7.4, pytest-5.2.0, py-1.8.0, pluggy-0.13.0
Django settings: django_fixtures.settings (from ini file)
rootdir: /django_fixtures, inifile: pytest.ini
plugins: django-3.5.1
collected 1 item

test.py E
[100%]
============================= ERRORS ======================================
_____________ ERROR at setup of test_should_create_two_users ______________

self = ,
sql ='INSERT INTO "auth_group" ("name") VALUES (%s) RETURNING "auth_group"."id"'
,params = ('app_user',)

def _execute(self, sql, params, *ignored_wrapper_args):
self.db.validate_no_broken_transaction()
with self.db.wrap_database_errors:
if params is None:
# params default might be backend specific.
return self.cursor.execute(sql)
else:
> return self.cursor.execute(sql, params)
E psycopg2.IntegrityError: duplicate key value violates
unique constraint "auth_group_name_key"
E DETAIL: Key (name)=(app_user) already exists.

Новый тест выдает IntegrityError. Сообщение об ошибке исходит из базы данных, поэтому оно может немного отличаться в зависимости от используемой базы данных. Согласно сообщению об ошибке, тест нарушает уникальное ограничение на имя группы. Когда вы смотрите на свои светильники, это имеет смысл. Группа «app_user» создается дважды: один раз в устройстве user_A и еще раз в устройстве user_B.

Интересное наблюдение, которое мы упустили до сих пор, заключается в том, что прибор user_A использует fixture db. Это означает, что приспособления можно вставлять в другие приспособления. Вы можете использовать эту функцию для устранения ошибки IntegrityError, указанной выше. Создайте группу «app_user» только один раз в фикстуре и вставьте ее в фикстуры user_A и user_B.

Для этого проведите рефакторинг теста и добавьте фикстуру группы «пользователь приложения»:
import pytest
from django.contrib.auth.models import User, Group, Permission

@pytest.fixture
def app_user_group(db) -> Group:
group = Group.objects.create(name="app_user")
change_user_permissions = Permission.objects.filter(
codename__in=["change_user", "view_user"],
)
group.permissions.add(*change_user_permissions)
return group

@pytest.fixture
def user_A(db, app_user_group: Group) -> User:
user = User.objects.create_user("A")
user.groups.add(app_user_group)
return user

@pytest.fixture
def user_B(db, app_user_group: Group) -> User:
user = User.objects.create_user("B")
user.groups.add(app_user_group)
return user

def test_should_create_two_users(user_A: User, user_B: User) -> None:
assert user_A.pk != user_B.pk

Запустите ваш тест:
$ pytest test.py
================================== test session starts ===============
platform linux -- Python 3.7.4, pytest-5.2.0, py-1.8.0, pluggy-0.13.0
Django settings: django_fixtures.settings (from ini file)
rootdir: /django_fixtures, inifile: pytest.ini
plugins: django-3.5.1
collected 1 item

test.py .

Потрясающе! Ваши тесты пройдены. Фиксатор группы инкапсулирует логику, относящуюся к группе «пользователь приложения», такую как установка разрешений. Затем вы вставили группу в два отдельных пользовательских устройства. Построив свои фикстуры таким образом, вы упростили чтение и поддержку тестов.

Использование фабрики
До сих пор вы создавали объекты с очень небольшим количеством аргументов. Однако некоторые объекты могут быть более сложными и содержать множество аргументов с множеством возможных значений. Для таких объектов вы можете создать несколько тестовых приборов.

Например, если вы предоставите все аргументы create_user (), прибор будет выглядеть так:

import pytest
from django.contrib.auth.models import User

@pytest.fixture
def user_A(db, app_user_group: Group) -> User
user = User.objects.create_user(
username="A",
password="secret",
first_name="haki",
last_name="benita",
email="",
is_staff=False,
is_superuser=False,
is_active=True,
)
user.groups.add(app_user_group)
return user

Ваша фикстура стала намного сложнее! Экземпляр пользователя теперь может иметь множество различных вариаций, таких как суперпользователь, штатный пользователь, неактивный штатный пользователь и неактивный обычный пользователь.

В предыдущих разделах вы узнали, что может быть сложно поддерживать сложную логику настройки в каждом тестовом устройстве. Итак, чтобы избежать повторения всех значений каждый раз при создании пользователя, добавьте функцию, которая использует create_user () для создания пользователя в соответствии с конкретными потребностями вашего приложения:

from typing import List, Optional
from django.contrib.auth.models import User, Group

def create_app_user(
username: str,
password: Optional[str] = None,
first_name: Optional[str] = "first name",
last_name: Optional[str] = "last name",
email: Optional[str] = "",
is_staff: str = False,
is_superuser: str = False,
is_active: str = True,
groups: List[Group] = [],
) -> User:
user = User.objects.create_user(
username=username,
password=password,
first_name=first_name,
last_name=last_name,
email=email,
is_staff=is_staff,
is_superuser=is_superuser,
is_active=is_active,
)
user.groups.add(*groups)
return user

Функция создает пользователя приложения. Каждый аргумент имеет разумное значение по умолчанию в соответствии с конкретными требованиями вашего приложения. Например, ваше приложение может требовать, чтобы у каждого пользователя был адрес электронной почты, но встроенная функция Django не обеспечивает такого ограничения. Вместо этого вы можете применить это требование в своей функции.

Функции и классы, создающие объекты, часто называют фабриками. Почему? Это потому, что эти функции действуют как фабрики, производящие экземпляры определенного класса. Чтобы узнать больше о фабриках в Python, ознакомьтесь с шаблоном фабричного метода и его реализацией в Python.

Вышеупомянутая функция является простой реализацией фабрики. Он не содержит состояния и не реализует сложную логику. Вы можете реорганизовать свои тесты, чтобы они использовали фабричную функцию для создания пользовательских экземпляров в ваших фикстурах:

@pytest.fixture
def user_A(db, app_user_group: Group) -> User:
return create_user(username="A", groups=[app_user_group])

@pytest.fixture
def user_B(db, app_user_group: Group) -> User:
return create_user(username="B", groups=[app_user_group])

def test_should_create_user(user_A: User, app_user_group: Group) -> None:
assert user_A.username == "A"
assert user_A.email == ""
assert user_A.groups.filter(pk=app_user_group.pk).exists()

def test_should_create_two_users(user_A: User, user_B: User) -> None:
assert user_A.pk != user_B.pk

Ваши приспособления стали короче, а ваши тесты теперь более устойчивы к изменениям. Например, если вы использовали настраиваемую модель пользователя и только что добавили в модель новое поле, вам нужно будет изменить только create_user (), чтобы ваши тесты работали должным образом.

Использование фабрик как приспособлений
Сложная логика настройки усложняет написание и сопровождение тестов, что делает весь пакет уязвимым и менее устойчивым к изменениям. До сих пор вы решали эту проблему, создавая фикстуры, создавая зависимости между фикстурами и используя фабрику, чтобы максимально абстрагироваться от логики настройки.

Но в ваших тестовых приборах еще есть некоторая логика настройки:

@pytest.fixture
def user_A(db, app_user_group: Group) -> User:
return create_user(username="A", groups=[app_user_group])

@pytest.fixture
def user_B(db, app_user_group: Group) -> User:
return create_user(username="B", groups=[app_user_group])

Обе фикстуры вводятся с app_user_group. В настоящее время это необходимо, поскольку фабричная функция create_user () не имеет доступа к фикстуре app_user_group. Наличие такой логики настройки в каждом тесте затрудняет внесение изменений и с большей вероятностью будет упущено из виду в будущих тестах. Вместо этого вы хотите инкапсулировать весь процесс создания пользователя и абстрагироваться от тестов. Таким образом, вы можете сосредоточиться на рассматриваемом сценарии, а не настраивать уникальные тестовые данные.

Чтобы предоставить пользовательской фабрике доступ к фикстуре app_user_group, вы можете использовать в качестве фикстуры шаблон под названием factory:

from typing import List, Optional

import pytest
from django.contrib.auth.models import User, Group, Permission

@pytest.fixture
def app_user_group(db) -> Group:
group = Group.objects.create(name="app_user")
change_user_permissions = Permission.objects.filter(
codename__in=["change_user", "view_user"],
)
group.permissions.add(*change_user_permissions)
return group

@pytest.fixture
def app_user_factory(db, app_user_group: Group):
# Closure
def create_app_user(
username: str,
password: Optional[str] = None,
first_name: Optional[str] = "first name",
last_name: Optional[str] = "last name",
email: Optional[str] = "",
is_staff: str = False,
is_superuser: str = False,
is_active: str = True,
groups: List[Group] = [],
) -> User:
user = User.objects.create_user(
username=username,
password=password,
first_name=first_name,
last_name=last_name,
email=email,
is_staff=is_staff,
is_superuser=is_superuser,
is_active=is_active,
)
user.groups.add(app_user_group)
# Add additional groups, if provided.
user.groups.add(*groups)
return user
return create_app_user

Это не тяжелее того, что вы уже сделали, поэтому давайте разберемся:

Приспособление app_user_group остается прежним. Он создает специальную группу «пользователь приложения» со всеми необходимыми разрешениями.

Добавляется новый прибор с именем app_user_factory, и он вводится вместе с прибором app_user_group.

Приспособление app_user_factory создает закрытие и возвращает внутреннюю функцию с именем create_app_user ().

create_app_user () похожа на функцию, которую вы реализовали ранее, но теперь у нее есть доступ к фикстуре app_user_group. Имея доступ к группе, вы теперь можете добавлять пользователей в app_user_group в заводской функции.

Чтобы использовать прибор app_user_factory, вставьте его в другое приспособление и используйте его для создания пользовательского экземпляра:

@pytest.fixture
def user_A(db, app_user_factory) -> User:
return app_user_factory("A")

@pytest.fixture
def user_B(db, app_user_factory) -> User:
return app_user_factory("B")

def test_should_create_user_in_app_user_group(
user_A: User,
app_user_group: Group,
) -> None:
assert user_A.groups.filter(pk=app_user_group.pk).exists()

def test_should_create_two_users(user_A: User, user_B: User) -> None:
assert user_A.pk != user_B.pk

Обратите внимание, что, в отличие от предыдущего, созданный вами прибор предоставляет функцию, а не объект. Это основная концепция, лежащая в основе фабрики как шаблона приспособлений: фабричное приспособление создает укупорку, которая обеспечивает внутреннюю функцию доступа к приспособлениям.

Теперь, когда у вас есть оборудование, это полный код для вашего теста:

from typing import List, Optional

import pytest
from django.contrib.auth.models import User, Group, Permission

@pytest.fixture
def app_user_group(db) -> Group:
group = Group.objects.create(name="app_user")
change_user_permissions = Permission.objects.filter(
codename__in=["change_user", "view_user"],
)
group.permissions.add(*change_user_permissions)
return group

@pytest.fixture
def app_user_factory(db, app_user_group: Group):
# Closure
def create_app_user(
username: str,
password: Optional[str] = None,
first_name: Optional[str] = "first name",
last_name: Optional[str] = "last name",
email: Optional[str] = "",
is_staff: str = False,
is_superuser: str = False,
is_active: str = True,
groups: List[Group] = [],
) -> User:
user = User.objects.create_user(
username=username,
password=password,
first_name=first_name,
last_name=last_name,
email=email,
is_staff=is_staff,
is_superuser=is_superuser,
is_active=is_active,
)
user.groups.add(app_user_group)
# Add additional groups, if provided.
user.groups.add(*groups)
return user
return create_app_user

@pytest.fixture
def user_A(db, app_user_factory) -> User:
return app_user_factory("A")

@pytest.fixture
def user_B(db, app_user_factory) -> User:
return app_user_factory("B")

def test_should_create_user_in_app_user_group(
user_A: User,
app_user_group: Group,
) -> None:
assert user_A.groups.filter(pk=app_user_group.pk).exists()

def test_should_create_two_users(user_A: User, user_B: User) -> None:
assert user_A.pk != user_B.pk

Откройте терминал и запустите тест:

$ pytest test.py
======================== test session starts ========================
platform linux -- Python 3.8.1, pytest-5.3.3, py-1.8.1, pluggy-0.13.1
django: settings: django_fixtures.settings (from ini)
rootdir: /django_fixtures/django_fixtures, inifile: pytest.ini
plugins: django-3.8.0
collected 2 items

test.py ..

Заключение
Вы успешно реализовали фабрику фикстур, которая предоставляет экземпляры модели Django. Вы также поддерживали и внедряли зависимости между фикстурами таким образом, чтобы избавиться от некоторых хлопот при написании и сопровождении тестов.

Из этого урока вы узнали:

Как создавать и загружать фикстуры в Django
Как предоставить тестовые инструменты для моделей Django в pytest
Как использовать фабрики для создания фикстур для моделей Django в pytest
Как реализовать фабрику в виде шаблона приспособлений для создания зависимостей между тестовыми приспособлениями
Теперь вы можете внедрять и поддерживать надежный набор тестов, который поможет вам быстрее и быстрее создавать лучший и надежный код!

перевод статьи "How to Provide Test Fixtures for Django Models in Pytest": 1 комментарий

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *