Интеграционные тесты: от теории к практике с реальными примерами

Интеграционные тесты: от теории к практике с реальными примерами

Интеграционные тесты — это мост между изолированными юнит-тестами и проверкой всей системы целиком. Они отвечают на главный вопрос: работают ли отдельные модули нашего приложения вместе так, как задумано? В этой статье мы глубоко погрузимся в суть интеграционного тестирования, разберем его ключевые принципы и, что самое важное, рассмотрим конкретные, рабочие примеры на разных технологических стеках.

Что такое интеграционные тесты и зачем они нужны?

Если юнит-тесты проверяют «кирпичики» (отдельные функции или классы) в идеальных лабораторных условиях, то интеграционные тесты проверяют, как эти кирпичики скреплены «раствором» — взаимодействуют через API, базы данных, файловые системы или внешние сервисы. Их цель — выявить проблемы на стыках компонентов: несовместимость форматов данных, ошибки в конфигурации, неправильное понимание контрактов между сервисами.

Ключевое отличие: Интеграционный тест всегда затрагивает более одного логического модуля и внешние зависимости (БД, кэш, API). Он проверяет сценарий, а не изолированную единицу кода.

Уровни и стратегии интеграционного тестирования

Интеграция может проводиться по разным сценариям:

  • Снизу вверх (Bottom-Up): Сначала тестируются низкоуровневые модули, затем к ним подключаются и проверяются высокоуровневые. Часто требует использования заглушек (stubs) для верхних уровней на начальном этапе.
  • Сверху вниз (Top-Down): Противоположный подход. Сначала тестируется высокоуровневая логика с заглушками для нижних модулей, которые затем постепенно заменяются реальными.
  • Большой взрыв (Big Bang): Все модули интегрируются разом, и затем проводится глобальное тестирование. Рискованный метод, так как сложно локализовать ошибку.

Реальные примеры интеграционных тестов

Теория без практики мертва. Давайте рассмотрим конкретные кейсы.

Пример 1: Тестирование сервиса с базой данных (Python + pytest)

Допустим, у нас есть сервис `UserRepository`, который сохраняет и загружает пользователей из PostgreSQL.

import pytest
import asyncpg
from app.repositories.user_repository import UserRepository

@pytest.mark.asyncio
async def test_user_repository_create_and_get():
    # 1. Поднимаем реальное тестовое подключение к БД (например, в Docker)
    conn = await asyncpg.connect(test_dsn)
    await conn.execute("TRUNCATE TABLE users CASCADE;")

    repo = UserRepository(conn)

    # 2. Действие: создаем пользователя
    user_id = await repo.create_user("test@mail.com", "Ivan")

    # 3. Проверка: можем ли мы получить его обратно?
    user = await repo.get_user_by_id(user_id)

    assert user is not None
    assert user.email == "test@mail.com"
    assert user.name == "Ivan"

    # 4. Обязательно чистим за собой (фикстуры в pytest делают это элегантнее)
    await conn.close()

Это классический интеграционный тест: он проверяет работу нашего репозитория вместе с реальной (или максимально приближенной) базой данных.

Пример 2: Тестирование REST API эндпоинта (Node.js + Jest + Supertest)

Здесь мы тестируем не логику функции, а целый HTTP-маршрут, его взаимодействие с фреймворком, middleware и, опционально, с базой данных.

const request = require('supertest');
const app = require('../app'); // Наше Express/Koa/Fastify приложение
const { initDb, closeDb } = require('./test-db-helper');

describe('POST /api/users', () => {
  beforeAll(async () => {
    await initDb(); // Запускаем тестовую БД
  });

  afterAll(async () => {
    await closeDb(); // Гасим соединение
  });

  it('should create a new user and return 201', async () => {
    const userData = { name: 'Anna', email: 'anna@test.ru' };

    // Supertest отправляет реальный HTTP-запрос к нашему приложению
    const response = await request(app)
      .post('/api/users')
      .send(userData)
      .set('Accept', 'application/json');

    expect(response.statusCode).toBe(201);
    expect(response.body).toHaveProperty('id');
    expect(response.body.name).toBe(userData.name);
    // Дополнительно можно проверить, что пользователь действительно появился в БД
  });
});

Важно: Такие тесты должны работать с изолированным тестовым окружением (отдельная БД, мок внешних платежных шлюзов), чтобы не влиять на продакшен и быть детерминированными.

Пример 3: Интеграция с внешним API (с использованием мок-сервера)

Часто наш сервис зависит от внешнего API (погода, курсы валют, SMS-шлюз). В интеграционных тестах мы можем поднять мок-сервер (например, с помощью WireMock или nock), который будет эмулировать ответы внешнего сервиса по заранее заданным сценариям.

// Псевдокод с использованием WireMock
@BeforeEach
void setUp() {
    wireMockServer.stubFor(
        get(urlPathEqualTo("/api/currency"))
            .willReturn(aResponse()
                .withHeader("Content-Type", "application/json")
                .withBody("{ \"rate\": 75.5 }"))
    );
}

@Test
void testOrderPriceInUSD() {
    // Наш сервис, вызывающий внешний API, теперь получит предсказуемый ответ от мока
    OrderService service = new OrderService("http://localhost:8080/api/currency");
    double price = service.calculatePriceInUSD(1000);
    assertEquals(13.245, price, 0.001); // 1000 / 75.5
}

Это проверяет интеграцию нашего кода с HTTP-клиентом и корректность разбора ответа.

Лучшие практики и инструменты

  • Изоляция и скорость: Используйте легковесные контейнеры (Docker Testcontainers) для поднятия реальных БД или мок-серверов. Не забывайте сбрасывать состояние между тестами.
  • Тестовые данные: Управляйте данными через фикстуры или миграции. Избегайте хардкода ID, которые могут быть в продакшен-БД.
  • Популярные инструменты:
    1. Testcontainers: Идеален для поднятия реальных служб (Postgres, Redis, Kafka) в Docker.
    2. WireMock, MockServer: Для мокирования HTTP-сервисов.
    3. DBUnit, Factory Boy: Для управления тестовыми данными в БД.

FAQ: Часто задаваемые вопросы об интеграционных тестах

Чем интеграционные тесты отличаются от end-to-end (E2E)?

E2E-тесты проверяют весь поток с точки зрения пользователя (например, через браузер с Selenium). Они максимально широкие и медленные. Интеграционные тесты фокусируются на взаимодействии конкретных модулей внутри системы, они уже, но быстрее и стабильнее, чем E2E.

Сколько интеграционных тестов нужно писать?

Значительно меньше, чем юнит-тестов. Они должны покрывать ключевые сценарии взаимодействия между важнейшими компонентами. Цель — не 100% coverage, а уверенность в корректности интеграции.

Можно ли обойтись только юнит-тестами?

Нет. Юнит-тесты, даже с моками всех зависимостей, не выявят проблем, которые возникают именно при реальном взаимодействии: различия в версиях API, ошибки сетевых таймаутов, несоответствие форматов данных в БД.

Интеграционные тесты — это дорого?

Да, их написание и поддержка сложнее, чем у юнит-тестов. Однако цена ошибки, найденной на продакшене из-за сбоя интеграции, почти всегда многократно выше затрат на написание этих тестов.