Тестирование приложений на Django — неотъемлемая часть профессиональной жизни веб-разработчиков на Python. Сюда входит написание различных тестов:
- Юнит-тестов для отдельных модулей
- Интеграционных тестов, проверяющих работоспособность всего приложения
В этом уроке мы научимся создавать интеграционные тесты для наших веб-приложений на Django.
Веб-приложения работают по сети, обрабатывая HTTP-запросы. Такое поведение придется повторять прямо в тестах или как-то имитировать. Django позволяет использовать оба подхода. Мы остановимся на подходе с подменой веб-сервера, чтобы ускорить запуск и выполнение тестов. В остальном эти тесты проверяют работу приложения от запроса до ответа, что дает очень высокую степень уверенности в том, что приложение работает.
Первый тест
Интеграционные тесты в Django связаны с маршрутами. Каждый тест — это запрос на конкретный адрес для тестирования конкретного маршрута. Количество тестов для одного маршрута может быть разным, но конкретный тест — это всегда запрос-ответ.
Начнем с примера. Предположим, что у нас есть маршрут /users/, который возвращает список пользователей. Тест на такой маршрут должен выполнить запрос на этот адрес. Вот как будет выглядеть структура файлов в этом случае:
.
├── manage.py
├── pyproject.toml
└── simple_blog
├── users
│ ├── admin.py
│ ├── apps.py
│ ├── __init__.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ └── __init__.py
│ ├── models.py
│ ├── tests.py
│ ├── urls.py
│ └── views.py
Тесты расположены в директории приложения simple_blog/users/.
Сам тест выглядит так:
from django.test import TestCase
from django.urls import reverse
class UsersTest(TestCase):
def test_users_list(self):
response = self.client.get(reverse("users:index"))
self.assertEqual(response.status_code, 200)
Файл тестов — это класс фреймворка unittest, в котором тестовые методы начинаются с test_
. Во время теста Django делает запрос через специальный объект client
. Разберем по шагам:
- Метод
get(reverse("users:index"))
формирует объект запроса к указанной странице. Кроме GET запроса, мы можем выполнить любой другой запрос. На самом деле здесь не происходит HTTP-вызова — запрос передается в приложение напрямую, поэтому тесты работают быстрее, чем с реальным веб-сервером - Метод
assertEqual(response.status_code, 200)
проверяет, что вернулся ответ 200. По необходимости можно проверить любой другой статус
Проверка на код ответа считается одной из базовых проверок. Она показывает, что приложение в целом отработало ожидаемо. При этом мы не можем с уверенностью сказать, что все правильно.
Например, мы ожидаем, что в теле ответа будет HTML определенной структуры с нужными данными, но вдруг там ничего нет? Для контроля ответа нужно добавить проверку тела ответа.
from django.test import TestCase
from django.urls import reverse
class UsersTest(TestCase):
def test_users_list(self):
response = self.client.get(reverse("users:index"))
self.assertEqual(response.status_code, 200)
# Проверяем наличие данных в контексте шаблона
self.assertIn('users', response.context)
users = response.context['users']
# Проверяем не пустой ли список пользователей
self.assertTrue(len(users) > 0)
И последний шаг — запуск тестов:
uv run manage.py test
Found 1 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 tests in 1.296s
OK
Destroying test database for alias 'default'...
Взаимодействие с базой
Пример теста списка пользователей не включает в себя одну важную деталь — наполнение базы данных. По умолчанию тесты используют ту базу данных, которая указана в конфигурации. За ее наполнение отвечает программист. Кроме наполнения базы, нам нужна еще и ее очистка.
Представьте, что мы написали тест, который создает пользователя. Если после теста мы не удалим этого пользователя, то следующий тест может завершиться с ошибкой — он не рассчитывает, что в базе уже есть такие данные. По этой причине в Django каждый тест выполняется в отдельной транзакции, которая откатывается в конце теста. Таким образом достигается полная изоляция тестов друг от друга.
Посмотрим на работу такого теста на примере запроса, обновляющего пользователя. Для этой операции используем маршрут /users/<pk>
. Для выполнения запроса нам понадобится идентификатор пользователя.
from django.test import TestCase
from django.urls import reverse
class UsersTest(TestCase):
def setUp(self):
# создаем пользователя в базе
Users.objects.create(name="John", email="johndoe@mail.com")
def test_update_user(self):
# обновляем пользователя
response = self.client.post(reverse("users:create", kwargs={"name": "Bob"}))
# проверим что пользователь изменен получив его по pk
user = Users.objects.get(pk=user.id)
self.assertEqual(user.name, "Bob")
Шаг 1. Сначала мы создаем пользователя. Для наполнения базы данными используется метод setUp()
.
def setUp(self):
# создаем пользователя в базе
Users.objects.create(name="John", email="johndoe@mail.com")
Шаг 2. Затем мы подготавливаем запрос. В самом запросе формируем правильный адрес, подставляя идентификатор созданного пользователя:
response = self.client.post(reverse("users:create", kwargs={"name": "Bob"}))
Шаг 3. Выполняем запрос и проверяем, что он действительно изменил пользователя в базе данных:
user = Users.objects.get(pk=user.id)
self.assertEqual(user.name, "Bob")
Кроме изменения данных в базе, имеет смысл протестировать ответ, который возвращается после запроса.
Обычно в простых сценариях взаимодействия все наполнение базы можно уместить в setUp()
. Но для ситуаций, когда требуется больше данных, лучше воспользоваться Django-фикстурами. Фикстура здесь это набор данных, чаще всего в формате JSON. Самым простым способом создать фикстуру будет сделать дамп, выгрузку данных, базы. Перед этим, разумеется нужно создать какие-то записи в базе.
uv run manage.py dumpdata
В результате Django сгенерирует JSON файл, который мы можем уже редактировать. Ниже пример фикстуры users.json
в одном из наших проектов.
[
{
"model": "users.user",
"pk": 1,
"fields": {
"password": "superpass",
"email": "johndoe@mail.com",
"name": "John",
}
},
{
"model": "users.user",
"pk": 2,
"fields": {
"password": "password123",
"name": "Alice",
"email": "alicesmith@email.com",
}
]
После чего фикстуру нужно сохранить в директории приложения fixtures и уже можем использовать ее в тестах. Django самостоятельно перед каждым тестом, еще до выполнения setUp()
, загрузит данные из фикстуры в тестовую базу.
from django.test import TestCase
from django.urls import reverse
class UsersTest(TestCase):
# указываем имена фикстур для загрузки
fixtures = ['users.json']
def test_update_user(self):
# обновляем пользователя
response = self.client.post(reverse("users:create", kwargs={"name": "Bob"}))
# проверим что пользователь изменен получив его по pk
user = Users.objects.get(pk=user.id)
self.assertEqual(user.name, "Bob")
Обратите внимание на важную деталь, связанную с интеграционными тестами. На протяжении урока мы писали тесты и убеждались, что приложение работает, даже не посмотрев на реализацию самого приложения. В этом и заключается суть интеграционных тестов. Нам не важно, как написано приложение внутри — мы убеждаемся только в том, что оно работает правильно. Из-за этого интеграционные тесты очень устойчивы к изменениям в коде, они меняются в основном из-за изменений API.
Выводы
Мы научились использовать встроенный механизм тестирования для написания тестов на Django. Django-тесты обладают важными преимуществами как бесшовная работа с базой данных, и абстракция браузера. С помощью их сочетания мы можем писать легко тесты проверяющие функционал больших частей приложения.

Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.