Перейти к содержимому
К программе курса
Pytest: Production-grade интеграционные тесты
2 / 922%

Integration vs Unit: границы тестирования

35 минут

Этот урок — фундамент курса. Мы разберём границу между unit и integration тестами и поймём, когда моки помогают, а когда вредят.

Главная цель: понять, что вы тестируете integration tests, а не unit tests с моками.

Что вы узнали в базовом курсе

В курсе "Pytest: С нуля до уверенности" вы научились:

✅ Писать unit tests
✅ Мокировать зависимости (HTTP API, файлы, время)
✅ Использовать фикстуры
✅ Проверять coverage

Это 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: фикстуры с транзакциями

Integration vs Unit: границы тестирования — Pytest: Production-grade интеграционные тесты — Potapov.me