Express / 37 минут

Интеграционные тесты API

Проверяйте HTTP-контракт Supertest, изоляцию состояния и отрицательные сценарии.

В процессе Теория: впереди Практика: впереди Тест: впереди
12 / 12
После этой главы
  • Проверяйте HTTP-контракт Supertest, изоляцию состояния и отрицательные сценарии.
  • Применить тему в задаче «Интеграционные тесты API» и объяснить граничные случаи.

Около 7 минут на чтение и разбор примеров. Практика — отдельно, в своём темпе.

Почему нужен запрос через HTTP-границу

Функция сервиса может правильно вернуть объект, но маршрут ошибётся в пути, middleware не разберёт body или ответ случайно раскроет лишнее поле. Интеграционный HTTP-тест проверяет совместную работу этих частей. Он не заменяет все unit-тесты, но ловит риски, которых прямой вызов чистой функции не видит.

Supertest позволяет направить запрос Express-приложению и проверить статус, заголовки и тело. Не нужно поднимать постоянный порт, который разделяется всеми тестами. Если приложение создаётся фабрикой с зависимостями, каждый сценарий может получить чистое хранилище и предсказуемые ответы без реального внешнего сервиса.

Базовая структура теста

Тест сначала создаёт app, затем выполняет запрос и проверяет наблюдаемый контракт. Удобно независимо утверждать статус и полезные поля JSON. Проверка только 200 недостаточна: handler может вернуть совершенно не те данные. У публичного пользователя отдельно полезно подтвердить отсутствие passwordHash, а не только наличие id.

JavaScript
const request = require('supertest');
const response = await request(app)
  .get('/health')
  .expect('content-type', /json/)
  .expect(200);
assert.deepEqual(response.body, { ok: true });

Этому фрагменту нужны импорт assert и подготовленное приложение из вашего тестового окружения. Сама логика проверки не зависит от имени тестового runner: он управляет запуском и сообщением о результате. Главное — дождаться Promise запроса, чтобы тест не завершился раньше фактической проверки.

Положительные и отрицательные сценарии

Для POST с непустым title проверьте обычную строку, пробелы, отсутствие поля и неверный тип. Для ресурса по id — существование, отсутствие и запрет доступа чужому пользователю. Для фильтра — корректную страницу, границы и недопустимую строку. Каждый пример должен проверять отдельное правило, а не повторять один успешный ответ с другим названием.

JavaScript
await request(app).post('/notes').send({ title: 'JS' }).expect(201);
await request(app).post('/notes').send({ title: '   ' }).expect(400);
await request(app).post('/notes').send({ title: 42 }).expect(400);

При отказе полезно также проверить отсутствие эффекта: заметка не создана, чужая запись не изменилась, XP не начислен второй раз. Статус может быть правильным при уже произошедшем нежелательном изменении. Поведение всей операции важнее отдельной строки ответа.

Изоляция данных и зависимостей

Не используйте один изменяемый массив всех тестов без явного сброса. Порядок запуска и параллельность способны изменить результаты. Для unit-теста сервиса подойдёт новый fake-репозиторий, для интеграции с БД — отдельная тестовая база и управляемая очистка. Никогда не направляйте тест удаления на пользовательскую production-базу ради удобства.

JavaScript
function createMemoryRepository() {
  const notes = new Map();
  return {
    find: id => notes.get(id),
    save: note => notes.set(note.id, note),
  };
}

Такая подмена не доказывает реальные свойства SQL-транзакций или уникальных ограничений. Для них нужна отдельная проверка настоящей инфраструктуры. Напротив, тест маршрута не обязан зависеть от живого почтового сервера, если задача — проверить выбор статуса и публичного DTO после успешного сценария.

Сессии, асинхронность и cleanup

Защищённые запросы проверяют с настоящим тестовым механизмом входа или контролируемой подменой authentication-границы. Успешный тест не должен случайно обходить саму проверяемую авторизацию. Cookies и ожидаемые Origin/CSRF-заголовки передают так же осознанно, как их использует настоящий клиент.

Серверы и ресурсы нужно закрывать в finally или lifecycle тестового окружения. Фиксированная задержка не является надёжным cleanup. Если async-операция отклоняется, тест должен дождаться и проверить нужную ветвь. Оставленные таймеры и соединения часто объясняют, почему «все assertions прошли», но процесс не завершился.

Практика набора параметров

Задание создаёт GET /double/:n и выполняет несколько запросов. Разрешены только цифры, поэтому '-1' и '12px' должны дать 400. Пустой список запросов возвращает пустой массив статусов. Реальная проверка параметра происходит внутри HTTP-маршрута, а не в обход приложения на входе solve.

После решения добавьте проверку содержимого успешного body и JSON-формы отказа. Для итогового API заметок составьте матрицу CRUD-сценариев и прав. Хороший набор тестов должен объяснять контракт будущему читателю и оставаться зелёным после безопасного переноса логики между helper-функциями без изменения поведения.

Загружаем редактор…
Ты дошёл до конца разделаЗакрепить знания в проекте

Музыка: Reverie — Scott Buckley · CC BY 4.0