← [Раздел](README.md) · [Главная](../README.md)

# Тестирование компонентов

## Цель

Разобрать **unit**, **интеграционные** и **контрактные** тесты для сервисов в облаке: изоляция с Testcontainers, моки внешних API, и как не сломать CI тяжёлыми зависимостями.

## Предварительно

- [README раздела 09](README.md)
- Docker установлен локально (для Testcontainers)

## Время

~80 минут

---

## Unit-тесты

Проверяют **одну единицу логики** без сети и диска.

```python
def test_apply_discount_caps_at_max():
    result = apply_discount(price=1000, percent=50, max_discount=300)
    assert result == 700  # скидка 300, не 500
```

| Хорошо | Плохо |
|--------|-------|
| Детерминированные | Зависимость от текущего времени без mock |
| Быстрые (< 10 мс) | Реальная БД |
| Имя описывает кейс | `test1`, `test2` |

Покрывайте **граничные случаи**: ноль, пусто, переполнение, невалидный ввод.

---

## Component tests

Тестируют **сервис целиком** с подменёнными границами:

```text
HTTP handler → service layer → mock repository
```

Или in-memory SQLite вместо PostgreSQL для быстрых проверок схемы SQL.

Цель: проверить **сборку слоёв** без поднятия всего кластера.

---

## Integration tests с Testcontainers

Реальный PostgreSQL в Docker на время теста:

```python
# псевдокод
with PostgresContainer("postgres:16") as pg:
    repo = OrderRepository(pg.get_connection_url())
    order = repo.create(user_id="u1", items=[...])
    assert repo.find(order.id).status == "pending"
```

| Плюсы | Минусы |
|-------|--------|
| Поведение как в prod | Медленнее unit |
| Ловит ошибки SQL/миграций | Нужен Docker в CI |

В CI кэшируйте образы и переиспользуйте контейнер на suite (где возможно).

---

## Тестирование HTTP API

| Подход | Инструменты |
|--------|-------------|
| In-process | `httpx.AsyncClient(app=app)` |
| Black-box | Запросы к `http://localhost:8080` |

Проверяйте:

- статус-код;
- тело JSON (schema);
- заголовки (`Content-Type`, `Idempotency-Key`).

```python
def test_create_order_returns_201(client):
    r = client.post("/orders", json={"sku": "BOOK-1", "qty": 1})
    assert r.status_code == 201
    assert "order_id" in r.json()
```

---

## Contract testing (Pact)

**Проблема:** команда A ждёт поле `total`, команда B отдала `amount` — prod падает.

**Решение:** потребитель пишет **ожидание**, провайдер **верифицирует** в CI.

```text
Consumer test → pact file → Provider verification job
```

Контракты особенно важны при **независимых релизах** микросервисов.

---

## Моки внешних сервисов

| Способ | Когда |
|--------|-------|
| **WireMock / MockServer** | HTTP банк, платёжка |
| **Запись (VCR)** | Редкие дорогие API |
| **Stub в docker-compose** | Локальная разработка |

Не мокайте то, что уже покрыто contract test с реальным провайдером в его pipeline.

---

## Фикстуры и данные

- **Factory** / builder для сущностей (`OrderFactory.build()`).
- **Не** копируйте prod дамп.
- Очистка БД между тестами: транзакция + rollback или truncate.

---

## Flaky tests

| Причина | Фикс |
|---------|------|
| `sleep(1)` | Ждать условие с timeout |
| Порядок тестов | Изоляция данных |
| Общий порт | Random port / testcontainers |
| Время | Freeze clock (freezegun) |

Flaky в main = команда перестаёт верить CI.

---

## Что гонять в CI на каждый PR

1. Unit (все).
2. Integration (критичные, < 5 мин).
3. Contract (если изменён API).
4. Lint + static analysis.

Полный E2E — после merge в main или nightly.

---

## Самопроверка

1. Чем component test отличается от unit?
2. Зачем Testcontainers вместо мока SQL?
3. Какую проблему решает Pact?
4. Назовите две причины flaky tests.

---

## Дальше

→ [Тестирование архитектуры](testirovanie-arhitektury.md)  
← [README раздела 09](README.md)
