Тестирование Express REST API с Jest и SuperTest

Введение
Тестирование может занимать время, но это обязательный этап разработки. Оно помогает находить ошибки на ранней стадии до релиза в продакшн. Здесь вы научитесь писать тесты для простого CRUD API на Express с использованием Jest (фреймворк тестирования) и SuperTest (HTTP-клиент для тестов).
Важно: пример использует локальный сервер на порту 3000 и хранение данных в оперативной памяти (массив). В реальных проектах вместо массива используют тестовую БД или мок объекты.
Что такое Jest
Jest — популярный фреймворк тестирования на JavaScript. Его проще всего начать использовать. Изначально создан Facebook для тестирования React, но прекрасно работает с Node.js и любыми JS-проектами. Jest включает в себя свой набор утверждений (assertions) и структуру для описания тестов (describe, test/it, expect).
Коротко о терминологии:
- Тест (test/it) — единичная проверка поведения.
- Сьют (describe) — группа тестов.
- Setup/Teardown (beforeAll, afterAll и т.д.) — подготовка и очистка окружения.
Что такое SuperTest
SuperTest — библиотека для тестирования HTTP в Node.js. Она строится на базе superagent и предоставляет объект request, который упрощает вызовы GET/POST/PUT/DELETE и получение ответа.
Пример использования SuperTest:
const request = require("supertest")
request("https://icanhazdadjoke.com")
.get('/slack')
.end(function(err, res) {
if (err) throw err;
console.log(res.body.attachments);
});Вы передаёте базовый URL в request, цепляете HTTP-метод и вызываете end() или используете async/await с .then(). Ответ затем проверяют с помощью Jest.
Создаём простой Express API
Для тестирования сначала нужно API. В примере API хранит задачи в массиве и поддерживает базовые операции CRUD.
Создайте папку и инициализируйте npm:
mkdir node-jest
npm init -yСоздайте файл index.js и запустите сервер:
const express = require("express")
const app = express()
app.use(express.json())
app.listen(3000, () => console.log("Listening at port 3000"))Добавим массив задач и маршруты далее по тексту.
Тестирование GET /todos
Этот endpoint возвращает все задачи. В index.js добавьте:
const todos = [
];
// Get all todos
app.get("/todos", (req, res) => {
return res.status(200).json({
data: todos,
error: null,
});
});Здесь ответ содержит статус 200 и JSON с полем data — массив задач, и поле error = null. Это то, что мы будем проверять.
Установите Jest и SuperTest:
npm install jest supertestИ добавьте скрипт тестов в package.json:
{
"scripts": {
"test": "jest"
}
}Быстрый пример базового теста в Jest
Рассмотрим простую функцию и её тест:
function sum(a, b) {
return a + b;
}
module.exports = sum;Файл теста:
const sum = require("./sum")
describe("Sum of two items", () => {
test("It should return 4", () => {
expect(sum(2,2)).toBe(4)
})
})describe группирует тесты, test/it описывает конкретный случай, expect проверяет результат.
Тестовый файл для API
Создайте api.test.js. Jest автоматически найдёт файлы с суффиксами .test или .spec.
В тесте используем SuperTest и указываем базовый URL:
const request = require("supertest")
const baseURL = "http://localhost:3000"Пример теста для GET /todos с подготовкой данных:
describe("GET /todos", () => {
const newTodo = {
id: crypto.randomUUID(),
item: "Drink water",
completed: false,
}
beforeAll(async () => {
// set up the todo
await request(baseURL).post("/todo").send(newTodo);
})
afterAll(async () => {
await request(baseURL).delete(`/todo/${newTodo.id}`)
})
it("should return 200", async () => {
const response = await request(baseURL).get("/todos");
expect(response.statusCode).toBe(200);
expect(response.body.error).toBe(null);
});
it("should return todos", async () => {
const response = await request(baseURL).get("/todos");
expect(response.body.data.length >= 1).toBe(true);
});
});Обратите внимание на beforeAll и afterAll: они инжектят тестовые данные и очищают их после теста. В продакшн-проектах лучше использовать отдельную тестовую БД или фабрики данных.
Тестирование POST /todo
Реализация эндпоинта в index.js:
app.post("/todo", (req, res) => {
try {
const { id, item, completed } = req.body;
const newTodo = {
id,
item,
completed,
};
todos.push(newTodo);
return res.status(201).json({
data: todos,
error: null,
});
} catch (error) {
return res.status(500).json({
data: null,
error: error,
});
}
});Тест отправляет тело запроса через send():
describe("POST /todo", () => {
const newTodo = {
// todo
}
afterAll(async () => {
await request(baseURL).delete(`/todo/${newTodo.id}`)
})
it("should add an item to todos array", async () => {
const response = await request(baseURL).post("/todo").send(newTodo);
const lastItem = response.body.data[response.body.data.length-1]
expect(response.statusCode).toBe(201);
expect(lastItem.item).toBe(newTodo["item"]);
expect(lastItem.completed).toBe(newTodo["completed"]);
});
});Проверяем код ответа 201 и то, что последний элемент массива совпадает с отправленным.
Тестирование PUT /todos/:id
Пример реализации в index.js:
app.put("/todos/:id", (req, res) => {
try {
const id = req.params.id
const todo = todos.find((todo) => todo.id == id);
if(!todo) {
throw new Error("Todo not found")
}
todo.completed = req.body.completed;
return res.status(201).json({
data: todo,
error: null,
});
} catch (error) {
return res.status(500).json({
data: null,
error: error,
});
}
});Тест обновления:
describe("Update one todo", () => {
const newTodo = {
// todo
}
beforeAll(async () => {
await request(baseURL).post("/todo").send(newTodo);
})
afterAll(async () => {
await request(baseURL).delete(`/todo/${newTodo.id}`)
})
it("should update item if it exists", async () => {
const response = await request(baseURL).put(`/todos/${newTodo.id}`).send({
completed: true,
});
expect(response.statusCode).toBe(201);
expect(response.body.data.completed).toBe(true);
});
});Проверяем, что поле completed стало true.
Тестирование DELETE /todos/:id
Реализация в index.js:
app.delete("/todos/:id", (req, res) => {
try {
const id = req.params.id
const todo = todos[0]
if(todo) {
todos.splice(id, 1)
}
return res.status(200).json({
data: todos,
error: null,
});
} catch (error) {
return res.status(500).json({
data: null,
error: error,
});
}
});Пример теста удаления:
describe("Delete one todo", () => {
const newTodo = {
// todo
}
beforeAll(async () => {
await request(baseURL).post("/todo").send(newTodo);
})
it("should delete one item", async () => {
const response = await request(baseURL).delete(`/todos/${newTodo.id}`);
const todos = response.body.data
const exists = todos.find(todo => {
newTodo.id == todoId
})
expect(exists).toBe(undefined)
});
});Проверяем, что массив в ответе не содержит удалённой задачи.
Частые ошибки и тонкости
- Не забывайте парсить JSON в Express: app.use(express.json()). Без этого req.body будет undefined.
- В тестах часто забывают запускать сервер до запуска тестов. Убедитесь, что сервер поднят (или экспортируйте app и запускайте его в тестовом окружении).
- Для параллельного запуска тестов используйте разные порты или мокируйте сетевые вызовы.
- Для интеграционных тестов применяйте тестовую базу данных или в памяти воссоздавайте состояние.
Важно: хранить данные только в оперативной памяти безопасно для демо. Для реальных приложений используйте отдельную тестовую БД и очищайте её между тестами.
Альтернативные подходы
- Mocha + Chai + SuperTest. Mocha предоставляет рантайм, Chai — утверждения. Это гибкая связка.
- AVA для быстрой параллельной работы тестов.
- Playwright / Cypress для end-to-end тестов с браузером.
Выбор зависит от уровня тестирования: unit, integration, e2e.
Ментальные модели и эвристики
- Тестовая пирамида: много unit-тестов, меньше интеграционных, ещё меньше e2e.
- Отделяйте тесты по областям: логика отдельно от API, API отдельно от интеграций с БД.
- Каждый тест должен быть детерминированным — одинаковый результат при одинаковом входе.
Уровни зрелости тестов
- Базовый: unit-тесты функций. Много маленьких тестов.
- Средний: интеграционные тесты API с моками для внешних сервисов.
- Высокий: end-to-end тесты с реальной БД и развёрнутым окружением в CI.
Чеклист перед добавлением теста
- Тест покрывает один сценарий.
- Нет побочных эффектов на внешний стейт.
- Данные теста изолированы и очищаются.
- Выполнение теста детерминировано.
- Время выполнения теста разумное (желательно < 200–500 мс для unit).
Примеры шаблонов и сниппеты
Подготовка тестового сервера (экспорт app в index.js):
// index.js
const express = require("express")
const app = express()
app.use(express.json())
// экспортируем app для тестов
module.exports = app
if (require.main === module) {
app.listen(3000, () => console.log("Listening at port 3000"))
}Тогда в тестах можно подключать app напрямую и запускать SuperTest без поднятия внешнего порта:
const request = require("supertest")
const app = require("./index")
describe("API tests without external server", () => {
it("should return 200", async () => {
const res = await request(app).get("/todos")
expect(res.statusCode).toBe(200)
})
})Это упрощает CI и устраняет гонки портов.
Критерии приёмки для API тестов
- Тесты проходят локально и в CI без ручного вмешательства.
- Для каждого endpoint есть сценарии успешного выполнения и основных ошибок.
- Тесты покрывают граничные значения (пустые данные, неверные id, неправильные типы).
- Тесты запускаются быстро при изменениях логики.
Когда такой подход не сработает
- Когда API зависит от внешних сервисов с нестабильным откликом — потребуется мока/стаб.
- Для высоконагруженных интеграционных тестов потребуется развёрнутая инфраструктура и тестовые дампы БД.
Интеграция в CI и советы
- Запускайте тесты в CI на каждом PR.
- Используйте отдельную ветку/пул для тестовой БД или Docker-контейнеры.
- Параллелите тесты, но убедитесь, что они независимы по данным.
Рекомендуемые тест-кейсы (минимум)
- GET /todos возвращает массив даже если он пуст.
- POST /todo с валидными данными возвращает 201 и содержит добавленный элемент.
- POST /todo с неполными данными возвращает 4xx и понятную ошибку.
- PUT /todos/:id меняет существующий объект и возвращает обновлённый объект.
- PUT /todos/:id для несуществующего id возвращает 404 или понятную ошибку.
- DELETE /todos/:id удаляет объект и он отсутствует в ответе.
Частая проверка кода ответа и структуры
Используйте не только статус-коды, но и форму тела ответа:
- statusCode должен быть ожидаемым (200, 201, 400, 404, 500).
- response.body должен иметь поля data и error независимо от результата.
- Для ошибок проверяйте, что error содержит информативную информацию.
Заключение
Вы научились писать базовые тесты для Express REST API с помощью Jest и SuperTest. Мы показали реализацию и тесты для GET, POST, PUT и DELETE, обсудили setup/teardown, альтернативные инструменты, чеклисты и рекомендации по CI. Примените эти шаблоны в своём проекте и расширяйте тесты по мере роста приложения.
Краткое резюме:
- Пишите детерминированные тесты и изолируйте данные.
- Используйте экспорт app для тестов без поднятия отдельного сервера.
- Подключайте тесты в CI и очищайте тестовую среду.
Важно: в production-проектах избегайте хранения тестовых данных в основной БД и используйте контейнеры или отдельные инстансы для тестов.
Похожие материалы
Как открыть WebP в Windows 11
Субтитры и аудиоописания в приложении Apple TV
Poe от Quora: обзор и руководство по созданию ботов
Как защитить ноутбук от кражи
График с двумя осями Y в Google Sheets