При создании приложения Django автоматически создается tests.py.
Большинство разработчиков время от времени задаются таким вопросом.

“Этот файл... разве его не можно удалить?”
“Как его вообще использовать?”

На самом деле tests.py — это официальная точка входа для тестирования, рекомендуемая Django, и,
как только вы немного привыкнете, он станет действительно большой страховкой при рефакторинге, добавлении функций и обновлении версий. (Проект Django)

В этой статье:

  • Что такое файл tests.py

  • Как его писать и выполнять

  • В каких ситуациях он особенно полезен

  • Примеры реального использования

все это будет рассмотрено в одном месте.


1. Что такое tests.py?



Django использует тестовую библиотеку на основе unittest из стандартной библиотеки Python.

  • Создайте тестовые классы, унаследованные от django.test.TestCase,

  • внутри которых напишите методы, начинающиеся с test_,

  • и они будут автоматически найдены и выполнены с помощью команды ./manage.py test.

Когда вы создаете приложение с помощью startapp, по умолчанию создается файл tests.py:

  • Для небольшого проекта — достаточно одного tests.py

  • При росте проекта — рекомендуется разбить на пакет tests/ (test_models.py, test_views.py и т.д.) в документации Django.

Таким образом, вы можете понимать это как основное местоположение для тестов на уровне приложения.


2. Когда это полезно? (Почему стоит использовать тесты)

Тестирование в Django особенно полезно в следующих ситуациях.

  1. Проверка моделей/бизнес-логики

    • Чем сложнее логика расчетов, изменения состояния, или пользовательские методы,

    • тем более уверенно можно рефакторить после создания теста.

  2. Проблемы с представлениями/URL/проверкой прав

    • Изменяется ли код ответа в зависимости от статуса аутентификации

    • Корректно ли обрабатывается некорректный ввод с перенаправлением / ошибкой

    • Передается ли правильный контекст в шаблон

  3. Предотвращение регрессии

    • Если вы поймали баг, задокументируйте его тестом, и он не повторится
  4. Обеспечение стабильности при обновлении версий

    • Когда вы поднимаете версию Django или библиотек

    • вы можете запустить ./manage.py test и убедиться, что все работает


3. Самый простой пример tests.py



3-1. Тестирование моделей

# myapp/tests.py
from django.test import TestCase
from .models import Article

class ArticleModelTests(TestCase):
    def setUp(self):
        self.article = Article.objects.create(
            title="Тестовый заголовок",
            content="Содержимое",
            views=0,
        )

    def test_increase_views(self):
        # given
        self.assertEqual(self.article.views, 0)

        # when
        self.article.increase_views()

        # then
        self.assertEqual(self.article.views, 1)
  • TestCase — это класс, расширяющий unittest.TestCase для Django.

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


4. Как тестировать представления/URL — используя Client

Django предоставляет тестовый HTTP-клиент django.test.Client.
Вы можете отправлять GET/POST запросы к URL и проверять ответы, не поднимая настоящий сервер.

4-1. Простой тест представления

# myapp/tests.py
from django.test import TestCase
from django.urls import reverse
from .models import Article

class ArticleViewTests(TestCase):
    def setUp(self):
        self.article = Article.objects.create(
            title="Статья 1",
            content="Содержимое",
            views=0,
        )

    def test_article_detail_page_returns_200(self):
        url = reverse("article_detail", args=[self.article.id])

        response = self.client.get(url)

        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "Статья 1")

Основные моменты:

  • self.client : Экземпляр тестового клиента, предоставляемый классом TestCase

  • reverse("article_detail", args=[...]) : Получение URL по имени вместо хардкодинга

  • assertContains : Проверка наличия определенной строки в теле ответа

4-2. Тестирование проверки прав и авторизации

from django.contrib.auth import get_user_model

class ArticlePermissionTests(TestCase):
    def setUp(self):
        self.user = get_user_model().objects.create_user(
            username="tester",
            password="pass1234",
        )
        self.article = Article.objects.create(
            title="секрет",
            content="Секретный текст",
            views=0,
        )

    def test_anonymous_user_redirected_to_login(self):
        url = reverse("article_edit", args=[self.article.id])

        response = self.client.get(url)

        self.assertEqual(response.status_code, 302)
        self.assertIn("/accounts/login", response["Location"])

    def test_logged_in_user_can_access_edit_page(self):
        self.client.login(username="tester", password="pass1234")
        url = reverse("article_edit", args=[self.article.id])

        response = self.client.get(url)

        self.assertEqual(response.status_code, 200)
  • необходимо проверить, происходит ли перенаправление при попытке доступа анонимного пользователя

  • нужно удостовериться, что авторизованный пользователь должен иметь доступ к странице редактирования

Это позволит на ранней стадии выявить ошибки связанные с правами.


5. Когда и как запускать tests.py?

5-1. Запуск всех тестов

$ python manage.py test
  • Он автоматически находит и выполняет
    файлы test*.py в текущем каталоге, содержащие под классы unittest.TestCase или django.test.TestCase.

5-2. Запуск только конкретного приложения

$ python manage.py test myapp

5-3. Запуск только конкретного модуля/класса/метода

# весь модуль myapp.tests
$ python manage.py test myapp.tests

# конкретный TestCase
$ python manage.py test myapp.tests.ArticleViewTests

# конкретный тест-метод
$ python manage.py test myapp.tests.ArticleViewTests.test_article_detail_page_returns_200

В процессе разработки полезно вырабатывать привычку быстро запускать только измененные части.


6. Как расширить tests.py в пакет тестов

Когда проект растет, одного tests.py становится недостаточно.
В документации Django также рекомендуется разбивать его на пакетную структуру.(Проект Django)

Например:

myapp/
    tests/
        __init__.py
        test_models.py
        test_views.py
        test_forms.py
        test_api.py

В каждом файле:

# myapp/tests/test_models.py
from django.test import TestCase
from myapp.models import Article

class ArticleModelTests(TestCase):
    ...

Разделение не влияет на выполнение.

$ python manage.py test myapp   # все
$ python manage.py test myapp.tests.test_models  # только тесты модели

7. Несколько реальных примеров использования

7-1. “Тест на воспроизведение ошибки” для предотвращения регрессии

  1. Ошибка в продакшене

  2. Напишите тест для воспроизведения в локальной среде (он должен упасть)

  3. Исправьте код

  4. Убедитесь, что тест проходит успешно

  5. Сделайте git коммит теста и исправленного кода вместе

Таким образом, в будущем, при рефакторинге, не возникнут те же самые ошибки.

7-2. Фиксация формата ответа API

Сотрудничая с фронтендом, формат ответа API становится очень важным.

class ArticleApiTests(TestCase):
    def setUp(self):
        self.article = Article.objects.create(
            title="API заголовок",
            content="Содержимое",
            views=0,
        )

    def test_article_detail_api_response(self):
        url = reverse("api:article-detail", args=[self.article.id])

        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)

        data = response.json()
        self.assertEqual(data["id"], self.article.id)
        self.assertIn("title", data)
        self.assertIn("content", data)
  • Явно укажите, какие поля обязательно должны быть включены в ответ

  • Если случайно изменить или удалить название поля, тест сразу же упадет

7-3. Тестирование логики валидации форм

from .forms import ArticleForm

class ArticleFormTests(TestCase):
    def test_title_is_required(self):
        form = ArticleForm(data={"title": "", "content": "Содержимое"})

        self.assertFalse(form.is_valid())
        self.assertIn("title", form.errors)

    def test_valid_form(self):
        form = ArticleForm(data={"title": "Заголовок", "content": "Содержимое"})

        self.assertTrue(form.is_valid())
  • Вы можете документировать правила валидации форм, оставляя их в виде тестов.

Django dev, пишущий тест py

8. Резюме – не удаляйте tests.py, попробуйте использовать его правильно один раз

В одном предложении:

tests.py — это файл, создающий “страховку для приложения Django”.

  • Сначала это может быть утомительно,

  • но как только вы привыкнете к разработке на основе тестов,

  • стресс при рефакторинге/добавлении функций/обновлении версий значительно снизится.

В реальных проектах:

  1. При создании новых функций – добавляйте как минимум 1–2 теста вместе с ними

  2. При исправлении ошибок – всегда сначала напишите тест для воспроизведения этой ошибки

  3. Когда начинаете масштабировать проект – выполните рефакторинг tests.py в пакет tests/

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