Зарегистрируйтесь, чтобы продолжить обучение

Интеграционные тесты Python: Разработка на фреймворке Django

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


Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
Программирование на Python, Разработка веб-приложений и сервисов используя Django, проектирование и реализация REST API
10 месяцев
с нуля
Старт 3 апреля

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»
Изображение Тото

Задавайте вопросы, если хотите обсудить теорию или упражнения. Команда поддержки Хекслета и опытные участники сообщества помогут найти ответы и решить задачу