Integration тесты, Selenium, Playwright, coverage, CI integration
Integration test проверяет соглашение между компонентами: URL → middleware → view → ORM → template/JSON. E2E добавляет настоящий HTTP-сервер, браузер и JavaScript. Чем выше уровень, тем дороже диагностика, поэтому браузер проверяет несколько критичных пользовательских путей, а детали остаются на более узких тестах.
Материал актуален для Django 5.2 LTS и 6.0.
| Граница | Инструмент | Чего нет |
|---|---|---|
| Pure Python | SimpleTestCase/unittest | ORM, middleware |
| Django request | Client/AsyncClient | socket, browser, JavaScript |
| Live HTTP | LiveServerTestCase | browser, если клиент обычный HTTP |
| Browser E2E | Playwright/Selenium + live server | внешняя production-инфраструктура |
Один и тот же test может называться component или integration в разных командах. Важнее записать, какие реальные зависимости он использует и какие заменены fake/mock.
from django.contrib.auth import get_user_model
from django.core import mail
from django.test import TestCase, override_settings
from django.urls import reverse
from shop.models import Order, Product
User = get_user_model()
@override_settings(
EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend",
)
class CheckoutFlowTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = User.objects.create_user(username="buyer")
cls.product = Product.objects.create(
title="Книга",
price="500.00",
)
def test_checkout_creates_order_and_confirmation(self):
self.client.force_login(self.user)
with self.captureOnCommitCallbacks(execute=True):
response = self.client.post(reverse("shop:checkout"), {
"product": self.product.pk,
"address": "Москва, Тверская, 1",
})
order = Order.objects.get(user=self.user)
self.assertRedirects(
response,
reverse("shop:order_detail", kwargs={"pk": order.pk}),
)
self.assertEqual(order.total, self.product.price)
self.assertEqual(len(mail.outbox), 1)Тест проверяет response, состояние базы и согласованный side effect. captureOnCommitCallbacks() нужен, если письмо планируется после commit внутри TestCase.
Не утверждайте только status 200: страница ошибки формы тоже может вернуть 200. Проверяйте redirect/шаблон, ошибки, созданные строки и отсутствие частичной записи.
Для checkout полезны tests:
Integration test ловит разрыв между формой, permission, транзакцией и моделью. Pure unit test каждого метода этого не гарантирует.
Не вызывайте реальный платёжный API, SMTP, S3 или broker из обычного test suite. Спрячьте SDK за adapter и подмените именно adapter:
from unittest.mock import patch
class PaymentTests(TestCase):
@patch("orders.services.payment_gateway.charge")
def test_gateway_decline_keeps_order_unpaid(self, charge):
charge.side_effect = PaymentDeclined("declined")
self.client.force_login(self.user)
response = self.client.post(
reverse("orders:pay", kwargs={"pk": self.order.pk}),
{"idempotency_key": "test-key-1"},
)
self.assertEqual(response.status_code, 409)
self.order.refresh_from_db()
self.assertEqual(self.order.status, Order.Status.PAYMENT_FAILED)Patch path указывает место использования имени. Mock всего SDK глубоко в реализации делает тест хрупким; свой маленький gateway protocol даёт стабильную границу.
Отдельный contract test можно запускать против sandbox провайдера по расписанию/перед release. Он использует отдельные credentials и ресурсы с cleanup, но не заменяет быстрый fake в каждом PR.
Celery eager mode и in-memory task backend не воспроизводят routing, serialization, acknowledgement, redelivery и worker crash. В основном suite проверяйте:
Нельзя делать вывод о delivery semantics только по task.delay.assert_called_once().
LiveServerTestCase поднимает Django на свободном TCP port и наследует transaction semantics TransactionTestCase. StaticLiveServerTestCase дополнительно обслуживает staticfiles через механизм разработки:
from django.contrib.staticfiles.testing import StaticLiveServerTestCaseОн не «отдаёт статику как production CDN»: не проверяет collectstatic, hashed manifest, compression и cache headers. Эти части проверяются build/deploy smoke tests.
Live server и test method работают в разных threads. На in-memory SQLite они могут разделять connection; не обращайтесь к нему одновременно из test thread и server thread. Production-like PostgreSQL уменьшает такие расхождения.
from django.contrib.auth import get_user_model
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from django.urls import reverse
from playwright.sync_api import expect, sync_playwright
class LoginE2ETests(StaticLiveServerTestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.playwright = sync_playwright().start()
cls.browser = cls.playwright.chromium.launch()
@classmethod
def tearDownClass(cls):
cls.browser.close()
cls.playwright.stop()
super().tearDownClass()
def setUp(self):
self.context = self.browser.new_context()
self.page = self.context.new_page()
def tearDown(self):
self.context.close()
def test_user_can_login(self):
get_user_model().objects.create_user(
username="alice",
password="secret-test-password",
)
self.page.goto(f"{self.live_server_url}{reverse('login')}")
self.page.get_by_label("Имя пользователя").fill("alice")
self.page.get_by_label("Пароль").fill("secret-test-password")
self.page.get_by_role("button", name="Войти").click()
expect(
self.page.get_by_role("heading", name="Личный кабинет")
).to_be_visible()
expect(self.page).to_have_url(
f"{self.live_server_url}{reverse('dashboard')}"
)Playwright auto-waits для действий и locator assertions. Метод locator.is_visible() возвращает состояние сразу и не является ожидающим assertion; используйте expect(locator).to_be_visible().
Выбирайте элементы по роли, label и видимому имени — это одновременно проверяет доступность. CSS-цепочки привязывают test к вёрстке. data-testid оставьте элементам без устойчивой пользовательской семантики.
Playwright поддерживает Chromium, Firefox и WebKit с единым API. Это не обещание «всегда быстрее Selenium» и не полная идентичность установленному Safari/Chrome; browser matrix выбирается из аналитики продукта. Selenium остаётся зрелой актуальной альтернативой.
Хорошие кандидаты:
Не переносите в браузер 40 вариантов field validation: Form tests быстрее и точнее. E2E проверяет, что компоненты соединены, сообщения видны, navigation работает и JavaScript не сломан.
У каждого сценария свои уникальные данные. Не полагайтесь на порядок тестов или общий аккаунт. Cleanup внешнего sandbox выполняется в finally/fixture teardown.
Flakiness — defect suite, а не нормальный шум. Основные меры:
time.sleep(); ждать наблюдаемое состояние;Не ждите «network idle» бездумно: analytics, websocket или polling могут не дать сети успокоиться. Ждите конкретный response или UI state.
coverage run --branch --source=src manage.py test
coverage report --show-missing
coverage xmlBranch coverage показывает непроверенную ветку лучше простого line coverage. Исключите migrations/generated code осознанно. Порог повышают постепенно и не заменяют им review набора сценариев: 100% execution без assertions не доказывает корректность.
Mutation testing и property-based tests могут проверить качество assertions для критичного pure/domain кода, но тоже требуют бюджета.
Минимальный job:
name: CI
on:
pull_request:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:17.5
env:
POSTGRES_USER: app
POSTGRES_PASSWORD: app
POSTGRES_DB: app
ports: ["5432:5432"]
options: >-
--health-cmd "pg_isready -U app"
--health-interval 5s
--health-timeout 5s
--health-retries 10
env:
DATABASE_URL: postgresql://app:app@127.0.0.1:5432/app
DJANGO_SETTINGS_MODULE: config.settings.test
SECRET_KEY: ci-only-not-a-production-secret
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- run: python -m pip install -r requirements/locked.txt
- run: python manage.py check
- run: python manage.py makemigrations --check --dry-run
- run: coverage run --branch manage.py test --parallel
- run: coverage report --fail-under=80Версию PostgreSQL в примере нужно синхронизировать с production и закреплять осознанно; плавающий latest делает CI невоспроизводимым. Dependency lock проверяется в репозитории. Для browser job добавьте установку Playwright browsers/system dependencies и загрузку traces/screenshots как artifacts при failure.
Обычно быстрые checks и integration идут на каждый PR. Небольшой критичный E2E smoke тоже полезен на PR; расширенную browser matrix можно запускать после merge/nightly. Если весь E2E отложен до ночи, broken checkout может попасть в main на целый день.
from rest_framework.test import APIClient, APITestCase
class PostApiTests(APITestCase):
def setUp(self):
self.client = APIClient()
self.client.force_authenticate(self.user)
def test_create_uses_authenticated_author(self):
response = self.client.post(
reverse("api:post-list"),
{"title": "API post", "author": self.other_user.pk},
format="json",
)
self.assertEqual(response.status_code, 201)
self.assertEqual(response.data["author"], self.user.pk)Такой test доказывает, что serializer не доверяет author из payload. Для проверки самой authentication используйте credentials/token; force_authenticate() намеренно обходит backend.
Selenium, LiveServerTestCase и Client остаются актуальными. Playwright — современная альтернатива, но его auto-wait не распространяется на любой произвольный boolean method. Старый шаблон click(); assert locator.is_visible() может быть flaky.
StaticLiveServerTestCase обслуживает staticfiles для теста, а не production pipeline. SQLite и eager task mode допустимы как быстрые doubles, если отдельный suite покрывает реальные PostgreSQL/broker semantics.
Далее: DRF: Сериализаторы