Integration тесты, Selenium, Playwright, coverage, CI integration
Unit-тесты проверяют функцию в изоляции. Но реальные баги часто живут на стыках: view + ORM + сериализатор + middleware + шаблон. Integration- и E2E-тесты проверяют, что компоненты работают вместе, имитируя поведение настоящего пользователя.
💡 Правило (пирамида тестов): много быстрых unit-тестов в основании, меньше integration-тестов посередине, совсем немного медленных E2E наверху. E2E ценны, но хрупки и медленны — не пишите E2E на то, что покрывается дешёвым unit-тестом.
| Уровень | Что проверяет | Скорость | Хрупкость |
|---|---|---|---|
| Unit | Одна функция/метод в изоляции | Очень быстро | Низкая |
| Integration | Несколько слоёв вместе (view → ORM → ответ) | Средне | Средняя |
| E2E | Весь стек через браузер, как пользователь | Медленно | Высокая |
⚠️ Антипаттерн «перевёрнутая пирамида»: много E2E и мало unit. Такой набор тестов идёт минутами, постоянно «моргает» (flaky) из-за тайминга браузера, и команда перестаёт ему доверять.
django.test.Client прогоняет настоящий HTTP-цикл: URL-резолвинг, middleware, view, рендеринг — но без реального сетевого сокета и браузера. Это рабочая лошадка integration-тестов.
from django.test import TestCase
from django.urls import reverse
from django.contrib.auth import get_user_model
class CheckoutFlowTest(TestCase):
def setUp(self):
self.user = get_user_model().objects.create_user('bob', password='pass12345')
self.product = Product.objects.create(title='Книга', price=500)
def test_full_purchase_flow(self):
# 1. Логин
self.client.login(username='bob', password='pass12345')
# 2. Добавление в корзину
resp = self.client.post(reverse('cart_add'), {'product_id': self.product.id})
self.assertEqual(resp.status_code, 302)
# 3. Оформление
resp = self.client.post(reverse('checkout'), {'address': 'ул. Ленина 1'})
# 4. Проверяем не только ответ, но и побочные эффекты в БД
self.assertEqual(resp.status_code, 200)
order = Order.objects.get(user=self.user)
self.assertEqual(order.status, 'created')
self.assertEqual(order.total, 500)💡 Хороший integration-тест проверяет и ответ, и побочные эффекты: создалась ли запись в БД, отправился ли email, поставилась ли задача в Celery. Один лишь
status_code == 200мало что гарантирует.
Тесты не должны ходить в реальный платёжный шлюз или слать письма. Изолируйте границы через mock и встроенные инструменты Django.
from unittest.mock import patch
from django.core import mail
class PaymentTest(TestCase):
@patch('orders.services.payment_gateway.charge')
def test_payment_called(self, mock_charge):
mock_charge.return_value = {'status': 'ok', 'tx_id': 'abc'}
self.client.force_login(self.user)
self.client.post(reverse('pay'), {'order_id': self.order.id})
mock_charge.assert_called_once() # шлюз вызван
self.assertEqual(len(mail.outbox), 1) # письмо «ушло» (в тестовый ящик)
self.assertIn('Оплата', mail.outbox[0].subject)⚠️ Каждый
TestCaseоборачивается в транзакцию и откатывается после — БД между тестами чистая. Но внешний мир Django не откатывает: реальные HTTP-вызовы, файлы, очереди нужно мокать или использовать тестовые бэкенды (locmemдля email,eagerдля Celery).
E2E прогоняет сценарий в настоящем браузере поверх запущенного сервера. Django поднимает его через LiveServerTestCase (реальный сокет, в отличие от Client).
Playwright современнее Selenium: авто-ожидания элементов (меньше flaky-тестов), быстрее, проще API.
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from playwright.sync_api import sync_playwright
class LoginE2ETest(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 test_user_can_login(self):
get_user_model().objects.create_user('alice', password='secret123')
page = self.browser.new_page()
page.goto(f'{self.live_server_url}/login/')
page.fill('#id_username', 'alice')
page.fill('#id_password', 'secret123')
page.click('button[type=submit]')
# Playwright сам дождётся появления текста — без time.sleep()
self.assertTrue(page.get_by_text('Личный кабинет').is_visible())💡 Используйте
StaticLiveServerTestCase(а неLiveServerTestCase), если страницы тянут собранную статику — он отдаёт её так же, как в проде.
⚠️ Главная причина flaky E2E — ручные
time.sleep()вместо ожидания состояния. Playwright/современный Selenium умеют ждать элемент/текст автоматически; полагайтесь на это, а не на фиксированные паузы.
Покрытие показывает, какие строки кода тесты вообще исполнили.
coverage run --source='.' manage.py test
coverage report -m # таблица с непокрытыми строками
coverage html # htmlcov/index.html — наглядный отчёт⚠️ 100% покрытие ≠ хорошие тесты. Можно исполнить строку, ничего по сути не проверив (нет ассертов на результат). Покрытие — индикатор «что вообще не тестируется», а не метрика качества. Гнаться за цифрой ради цифры вредно.
Тесты ценны, только если запускаются автоматически на каждый push/PR.
# .github/workflows/ci.yml (упрощённо)
jobs:
test:
services:
postgres:
image: postgres:15
env: { POSTGRES_PASSWORD: postgres }
steps:
- uses: actions/checkout@v4
- run: pip install -r requirements.txt
- run: python manage.py migrate
- run: coverage run --source='.' manage.py test
- run: coverage report --fail-under=80 # порог — ниже не пропускаем💡 E2E-тесты медленные — часто их выносят в отдельный, более редкий job (nightly или только на merge в main), чтобы не тормозить каждый PR.
class RegistrationTest(TestCase):
def test_registration_creates_user_and_sends_email(self):
resp = self.client.post(reverse('register'), {
'username': 'newbie',
'email': 'new@example.com',
'password1': 'StrongPass123',
'password2': 'StrongPass123',
})
# редирект после успеха
self.assertRedirects(resp, reverse('login'))
# пользователь в БД и пока неактивен (ждёт подтверждения)
user = get_user_model().objects.get(username='newbie')
self.assertFalse(user.is_active)
# письмо с подтверждением отправлено
self.assertEqual(len(mail.outbox), 1)
self.assertIn('подтверд', mail.outbox[0].subject.lower())Client, проверяя ответ И побочные эффектыLiveServerTestCasetime.sleep() — полагаетесь на авто-ожиданияВопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.
Далее: DRF: Сериализаторы