Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. Тестирование: Integration & E2E
testing_integration

Тестирование: Integration & E2E

Integration тесты, Selenium, Playwright, coverage, CI integration

Integration и E2E-тестирование Django

Integration test проверяет соглашение между компонентами: URL → middleware → view → ORM → template/JSON. E2E добавляет настоящий HTTP-сервер, браузер и JavaScript. Чем выше уровень, тем дороже диагностика, поэтому браузер проверяет несколько критичных пользовательских путей, а детали остаются на более узких тестах.

Материал актуален для Django 5.2 LTS и 6.0.

#Не спорьте о названии — определите границу

ГраницаИнструментЧего нет
Pure PythonSimpleTestCase/unittestORM, middleware
Django requestClient/AsyncClientsocket, browser, JavaScript
Live HTTPLiveServerTestCasebrowser, если клиент обычный HTTP
Browser E2EPlaywright/Selenium + live serverвнешняя production-инфраструктура

Один и тот же test может называться component или integration в разных командах. Важнее записать, какие реальные зависимости он использует и какие заменены fake/mock.

#Client: главный integration-инструмент

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/шаблон, ошибки, созданные строки и отсутствие частичной записи.

#Негативный сценарий важнее второго happy path

Для checkout полезны tests:

  • товар недоступен — order не создан;
  • цена из POST игнорируется, используется серверная цена;
  • повторный idempotency key не создаёт второй order;
  • чужой address/payment method недоступен;
  • gateway decline оставляет согласованный status;
  • callback поставлен только после commit.

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.

#Tasks и broker

Celery eager mode и in-memory task backend не воспроизводят routing, serialization, acknowledgement, redelivery и worker crash. В основном suite проверяйте:

  1. use case создал outbox/job после commit;
  2. task-функция корректна как обычная функция;
  3. один небольшой integration suite с реальным broker/worker проверяет wiring.

Нельзя делать вывод о delivery semantics только по task.delay.assert_called_once().

#Live server

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 уменьшает такие расхождения.

#Playwright: используйте locator assertions

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 остаётся зрелой актуальной альтернативой.

#Что действительно проверять E2E

Хорошие кандидаты:

  • login/logout/password reset;
  • регистрация с подтверждением;
  • основной checkout/subscription flow в fake/sandbox режиме;
  • критичная object permission через UI;
  • upload и download пользовательского файла;
  • один smoke сценарий admin/operations.

Не переносите в браузер 40 вариантов field validation: Form tests быстрее и точнее. E2E проверяет, что компоненты соединены, сообщения видны, navigation работает и JavaScript не сломан.

У каждого сценария свои уникальные данные. Не полагайтесь на порядок тестов или общий аккаунт. Cleanup внешнего sandbox выполняется в finally/fixture teardown.

#Борьба с flaky tests

Flakiness — defect suite, а не нормальный шум. Основные меры:

  • никаких time.sleep(); ждать наблюдаемое состояние;
  • не использовать общий mutable test account;
  • фиксировать timezone/clock там, где он влияет;
  • изолировать network и случайность;
  • сохранять screenshot, DOM, console, network trace и Playwright trace при падении;
  • воспроизводить тест отдельно и в random order;
  • retry использовать как временную диагностику, не как зелёную краску.

Не ждите «network idle» бездумно: analytics, websocket или polling могут не дать сети успокоиться. Ждите конкретный response или UI state.

#Coverage отвечает только «исполнялась ли строка»

coverage run --branch --source=src manage.py test coverage report --show-missing coverage xml

Branch coverage показывает непроверенную ветку лучше простого line coverage. Исключите migrations/generated code осознанно. Порог повышают постепенно и не заменяют им review набора сценариев: 100% execution без assertions не доказывает корректность.

Mutation testing и property-based tests могут проверить качество assertions для критичного pure/domain кода, но тоже требуют бюджета.

#CI должен повторять важные production свойства

Минимальный 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 на целый день.

#DRF integration

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.

#Legacy и актуальность

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.

#Документация

  • Django testing overview
  • Live server test case
  • Playwright assertions
  • coverage.py

Далее: DRF: Сериализаторы