Integration vs Unit: границы тестирования
Этот урок — фундамент курса. Мы разберём границу между unit и integration тестами и поймём, когда моки помогают, а когда вредят.
Главная цель: понять, что вы тестируете integration tests, а не unit tests с моками.
Что вы узнали в базовом курсе
В курсе "Pytest: С нуля до уверенности" вы научились:
Это unit testing — тестирование изолированных компонентов.
Проблема unit tests: ложная уверенность
Пример: сервис отправки email
# email_service.py
class EmailService:
def __init__(self, smtp_client):
self.smtp = smtp_client
def send_welcome(self, user_email: str):
subject = "Welcome!"
body = f"Hello {user_email}"
self.smtp.send(to=user_email, subject=subject, body=body)Unit тест (с моком):
# tests/test_email_service.py
from unittest.mock import Mock
def test_send_welcome():
"""Unit test — изолированный"""
mock_smtp = Mock()
service = EmailService(mock_smtp)
service.send_welcome("user@test.com")
# Проверяем что метод вызвался
mock_smtp.send.assert_called_once_with(
to="user@test.com",
subject="Welcome!",
body="Hello user@test.com"
)✅ Тест проходит!
Но в production:
# Реальный SMTP клиент
class SMTPClient:
def send(self, recipient, title, message): # ❌ Другие параметры!
# Отправка email
pass❌ Production падает! API не совпадает: to vs recipient, subject vs title, body vs message.
Проблема: Unit тест проверил что метод вызвался, но не проверил что интеграция работает.
Что такое integration test?
Integration test — тест, который проверяет взаимодействие между компонентами.
Integration тест (без мока):
# tests/test_email_integration.py
import pytest
from email_service import EmailService
from smtp_client import SMTPClient # Реальный клиент!
@pytest.fixture
def smtp_client():
"""Реальный SMTP клиент (тестовый режим)"""
return SMTPClient(host="localhost", port=1025) # Fake SMTP server
def test_send_welcome_integration(smtp_client):
"""Integration test — реальная интеграция"""
service = EmailService(smtp_client)
service.send_welcome("user@test.com")
# Проверяем что email РЕАЛЬНО отправился
# (можем проверить через fake SMTP server)Если API не совпадает — тест упадёт!
Пирамида тестирования
/\
/ \ E2E Tests (UI, API, slow)
/ \ ← Мало тестов, медленные
/------\
/ \ Integration Tests
/ \ ← Средне тестов, средняя скорость
/------------\
/ \ Unit Tests
/________________\ ← Много тестов, быстрыеПравило:
- 70% unit tests (быстрые, изолированные)
- 20% integration tests (средние, реальные зависимости)
- 10% E2E tests (медленные, полный сценарий)
Почему не 100% unit tests? Моки дают ложную уверенность — тесты зелёные, production падает.
Когда использовать моки, когда реальные зависимости?
✅ Мокируйте: внешние сервисы
Плохо контролируемые:
- HTTP API третьих сторон (Stripe, AWS)
- Email-серверы
- SMS-гейтвеи
- External webhooks
Почему мокировать:
- ❌ Медленно (сетевые запросы)
- ❌ Дорого (платные API)
- ❌ Нестабильно (сервис может упасть)
- ❌ Сложно настроить (нужны credentials)
Пример:
# ✅ МОКИРУЕМ Stripe API
from unittest.mock import patch
@patch('stripe.Charge.create')
def test_payment(mock_stripe):
mock_stripe.return_value = {"id": "ch_123", "status": "succeeded"}
result = process_payment(100, "usd")
assert result["status"] == "succeeded"✅ Используйте реальные: внутренние компоненты
Под вашим контролем:
- PostgreSQL, Redis (в Docker!)
- Ваш REST API
- Ваши классы и модули
- Файловая система (в /tmp)
Почему реальные:
- ✅ Проверяют реальную интеграцию
- ✅ Ловят ошибки совместимости
- ✅ Тестируют SQL-запросы, constraints
- ✅ Можно контролировать (Docker, testcontainers)
Пример:
# ✅ ИСПОЛЬЗУЕМ реальный PostgreSQL
import psycopg2
def test_create_user(db_connection):
"""Реальная БД в тесте"""
cur = db_connection.cursor()
cur.execute("INSERT INTO users (email) VALUES (%s)", ("user@test.com",))
cur.execute("SELECT email FROM users WHERE email=%s", ("user@test.com",))
result = cur.fetchone()
assert result[0] == "user@test.com"Test Doubles: типы заглушек
Test Double — общий термин для заглушек в тестах.
1. Dummy
Объект который передаётся но не используется.
def test_user_registration():
logger = None # Dummy — не используется в тесте
service = UserService(logger)
service.register("user@test.com")2. Stub
Возвращает заранее заданные данные.
class StubUserRepository:
def get_user(self, user_id):
return {"id": 1, "email": "stub@test.com"} # Всегда одно и то же
def test_get_user():
repo = StubUserRepository()
user = repo.get_user(123)
assert user["email"] == "stub@test.com"3. Spy
Записывает как его вызывали.
class SpyEmailService:
def __init__(self):
self.calls = []
def send(self, to, subject, body):
self.calls.append({"to": to, "subject": subject, "body": body})
def test_send_email():
spy = SpyEmailService()
spy.send("user@test.com", "Hi", "Hello")
assert len(spy.calls) == 1
assert spy.calls[0]["to"] == "user@test.com"4. Mock
Проверяет что методы вызвались правильно.
from unittest.mock import Mock
def test_with_mock():
mock_smtp = Mock()
service = EmailService(mock_smtp)
service.send_welcome("user@test.com")
# Mock проверяет вызов
mock_smtp.send.assert_called_once_with(
to="user@test.com",
subject="Welcome!",
body="Hello user@test.com"
)5. Fake
Работающая упрощённая реализация.
class FakeDatabase:
"""Работающая БД в памяти"""
def __init__(self):
self.users = {}
def insert(self, user_id, email):
self.users[user_id] = email
def get(self, user_id):
return self.users.get(user_id)
def test_with_fake():
db = FakeDatabase()
db.insert(1, "user@test.com")
assert db.get(1) == "user@test.com"Integration test чаще использует Fake или реальные зависимости.
Социальные vs Одиночные тесты
Одиночные тесты (Solitary Tests)
Изолируют тестируемый класс от ВСЕХ зависимостей.
def test_solitary():
"""Всё замоканно"""
mock_db = Mock()
mock_cache = Mock()
mock_logger = Mock()
service = UserService(mock_db, mock_cache, mock_logger)
service.create_user("user@test.com")
# Проверяем только тестируемый классПлюсы:
- ✅ Быстрые
- ✅ Изолированные (один класс)
Минусы:
- ❌ Не тестируют интеграцию
- ❌ Много boilerplate (моки)
Социальные тесты (Sociable Tests)
Используют реальные зависимости где возможно.
def test_sociable(db_connection):
"""Реальная БД, реальный кеш"""
cache = RealCache()
logger = RealLogger()
service = UserService(db_connection, cache, logger)
service.create_user("user@test.com")
# Проверяем результат в реальной БД
user = db_connection.execute("SELECT * FROM users WHERE email=?", ("user@test.com",))
assert user is not NoneПлюсы:
- ✅ Тестируют реальную интеграцию
- ✅ Ловят ошибки API
Минусы:
- ❌ Медленнее
- ❌ Требуют setup (БД, Docker)
Integration tests = sociable tests.
Практика: когда моки вредят
Пример: сохранение пользователя
Unit тест (с моком):
def test_save_user_unit():
mock_db = Mock()
repo = UserRepository(mock_db)
repo.save({"id": 1, "email": "user@test.com"})
# Проверяем что insert вызвался
mock_db.insert.assert_called_once() # ✅ ПроходитIntegration тест (реальная БД):
def test_save_user_integration(db_connection):
repo = UserRepository(db_connection)
repo.save({"id": 1, "email": "user@test.com"})
# Проверяем что данные РЕАЛЬНО сохранились
cur = db_connection.cursor()
cur.execute("SELECT email FROM users WHERE id=1")
result = cur.fetchone()
assert result[0] == "user@test.com" # ✅ Проверили реальностьЧто может пойти не так с моком:
- ❌ SQL-запрос неправильный (мок не проверит)
- ❌ Constraint violation (мок не проверит)
- ❌ Тип данных не совпадает (мок не проверит)
Integration тест ловит всё это.
Правило: мокируйте края, тестируйте ядро
┌─────────────────────────────────────┐
│ External Services (MOCK) │ ← Мокируем
│ - Stripe API │
│ - Email SMTP │
│ - AWS S3 │
└──────────────┬──────────────────────┘
│
┌──────────────▼──────────────────────┐
│ Your Application (REAL) │ ← Интеграция
│ - Business Logic │
│ - Database │
│ - Internal APIs │
└─────────────────────────────────────┘Мокируйте только то, что не контролируете.
Что вы узнали
✅ Integration test проверяет взаимодействие компонентов ✅ Моки дают ложную уверенность если переиспользуются ✅ Пирамида тестирования: 70% unit, 20% integration, 10% E2E ✅ Test Doubles: Dummy, Stub, Spy, Mock, Fake ✅ Социальные тесты используют реальные зависимости ✅ Правило: мокируйте края (внешние API), тестируйте ядро (БД, ваш код)
Следующий урок
Теперь вы понимаете границу между unit и integration. В следующем уроке настроим PostgreSQL фикстуры и начнём писать integration tests с реальной БД.
Переходите к уроку 1: PostgreSQL: фикстуры с транзакциями