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

Что вы узнаете (основная цель)
- Как настроить Jest и SuperTest для тестирования HTTP-эндпойнтов.
- Примеры тестов для GET /todos, POST /todo, PUT /todos/:id и DELETE /todos/:id.
- Практические рекомендации: подготовка окружения, фикстуры, изоляция тестов и обработка ошибок.
Что такое Jest?
Jest — это фреймворк для тестирования на JavaScript, изначально разработанный для проектов на React. Он включает раннер тестов и собственную систему утверждений (assertions). В контексте Node.js Jest хорошо подходит для unit- и интеграционных тестов благодаря простоте конфигурации и поддержке асинхронных тестов.
Определение: unit-тест — тест, проверяющий одну небольшую единицу кода. Интеграционный тест — проверяет взаимодействие нескольких компонентов (например, HTTP-сервер + обработчики).
Что такое SuperTest?
SuperTest — библиотека для тестирования HTTP в Node.js. На базе superagent, она предоставляет объект request, которому можно передать базовый URL и затем вызывать .get(), .post(), .put(), .delete() и другие методы для отправки HTTP-запросов. SuperTest удобен для интеграционных тестов API, когда необходимо симулировать реальное HTTP-взаимодействие.
Пример использования 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, затем вызываете метод и обрабатываете ответ в .end(). В Jest обычно используют async/await и методы библиотеки, чтобы сравнивать ожидаемые и фактические значения.
Создание простого Express API
Чтобы тестировать эндпойнты, сначала создайте минимальный REST API. Мы реализуем CRUD над массивом todos в памяти.
- Создайте директорию и инициализируйте npm:
mkdir node-jest
npm init -y- Установите зависимости (включая express) и инструменты для тестов:
npm install express
npm install --save-dev jest supertest- Создайте файл index.js с базовым сервером. Обратите внимание: для чтения body в Express используйте express.json().
const express = require("express")
const app = express()
app.use(express.json())
const todos = []
app.listen(3000, () => console.log("Listening at port 3000"))
// Get all todos
app.get("/todos", (req, res) => {
return res.status(200).json({
data: todos,
error: null,
});
});
// Create todo
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 });
}
});
// Update todo
app.put("/todos/:id", (req, res) => {
try {
const id = req.params.id;
const todo = todos.find((t) => t.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 });
}
});
// Delete todo
app.delete("/todos/:id", (req, res) => {
try {
const id = req.params.id;
const index = todos.findIndex((t) => t.id == id);
if (index !== -1) {
todos.splice(index, 1);
}
return res.status(200).json({ data: todos, error: null });
} catch (error) {
return res.status(500).json({ data: null, error: error });
}
});
module.exports = app; // экспорт для тестов (если запускать через supertest напрямую)Важно: в продакшн-коде вместо in-memory массива используйте базу данных и мокайте/фейковайте её в тестах.
Настройка Jest
Добавьте в package.json скрипт тестов:
{
"scripts": {
"test": "jest"
}
}Jest по умолчанию найдёт файлы с суффиксом .test.js или .spec.js.
Тестирование GET /todos
Создайте файл api.test.js в корне проекта. Для запусков HTTP-запросов используем SuperTest.
const request = require("supertest")
const baseURL = "http://localhost:3000"
const crypto = require('crypto')
describe("GET /todos", () => {
const newTodo = {
id: crypto.randomUUID(),
item: "Drink water",
completed: false,
}
beforeAll(async () => {
// создаём тестовые данные
await request(baseURL).post("/todo").send(newTodo);
})
afterAll(async () => {
await request(baseURL).delete(`/todos/${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
POST требует отправки JSON-тела. Пример теста:
describe("POST /todo", () => {
const newTodo = {
id: crypto.randomUUID(),
item: "Write tests",
completed: false,
}
afterAll(async () => {
await request(baseURL).delete(`/todos/${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);
});
});Совет: для надёжности проверяйте не только статус, но и структуру ответа (schema), например с помощью jest-extended или AJV.
Тестирование PUT /todos/:id
Пример обновления:
describe("Update one todo", () => {
const newTodo = {
id: crypto.randomUUID(),
item: "Temporary todo",
completed: false,
}
beforeAll(async () => {
await request(baseURL).post("/todo").send(newTodo);
})
afterAll(async () => {
await request(baseURL).delete(`/todos/${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);
});
});Проверьте также поведение при отсутствии элемента (404 или 500 в примере с throw — лучше вернуть 404).
Тестирование DELETE /todos/:id
Пример теста на удаление:
describe("Delete one todo", () => {
const newTodo = {
id: crypto.randomUUID(),
item: "To be deleted",
completed: false,
}
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 => todo.id == newTodo.id);
expect(exists).toBe(undefined);
});
});Внимание: в реализации DELETE важно удалять по индексу найденного элемента, а не по id как числу, если id — строка.
Практические советы и шаблоны
- Запускайте сервер в режиме тестов: используйте отдельный порт или экспортируйте app и подключайте через SuperTest без слушания порта.
- Деляйте тесты на unit и интеграционные: unit — без сети/БД, интеграционные — с реальным сервером/тестовой БД.
- Фикстуры: храните шаблоны тестовых данных в отдельной папке fixtures/.
- Идемпотентность: каждый тест должен убирать за собой данные (setup/teardown).
- Таймауты: если тесты асинхронные, задавайте разумные таймауты в Jest (jest.setTimeout).
- Схема ответа: проверяйте минимальную схему (наличие data, error и полей внутри data).
Мини-методика для разработки тестов (3 шага):
- Определите контракт эндпойнта (метод, путь, тело запроса, структура ответа, статус-коды).
- Напишите позитивный тест (happy path) и 2–3 негативных (ошибки валидации, несуществующие ресурсы).
- Автоматизируйте запуск в CI и фиксируйте flakiness (нестабильность) тестов.
Чек-листы по ролям
Developer:
- Экспортировать app для SuperTest
- Подключить express.json()
- Возвращать корректные HTTP-коды
- Именовать маршруты последовательно
QA / Тестировщик:
- Написать позитивные и негативные сценарии
- Проверить кейсы гонок при параллельных запросах
- Убедиться в idempotency для безопасных операций
DevOps / CI:
- Запустить тесты в изолированном окружении
- Настроить переменные окружения для тестовой БД
- Добавить отчётность по покрытию тестов
План отладки и отката (инцидент)
- При падении теста локально — воспроизвести последовательность запросов через curl/Postman.
- Проверить логи сервера на исключения и стек-трейсы.
- Если проблема в тестовых данных — откатить фикстуры и повторно прогнать тесты.
- В CI — откатить последний мердж, пока не выяснена причина.
Критерии приёмки
- Для каждого эндпойнта есть по крайней мере один позитивный и один негативный тест.
- Тесты стабильны в CI (отсутствие флейков > 95% успешных прогонов).
- API возвращает ожидаемые HTTP-коды и корректную структуру JSON.
Частые ошибки и подводные камни
- Не подключён express.json() — req.body будет undefined.
- Тесты зависят от состояния (неочищаемые данные) — приводят к флейкам.
- Использование одного и того же id в параллельных тестах — гонки.
- Неправильная обработка ошибок: бросать исключения вместо возврата корректного кода (404/400).
Краткий глоссарий
- Jest — тест-раннер и библиотека утверждений.
- SuperTest — инструмент для отправки HTTP-запросов в тестах.
- Фикстура — предустановленные данные для теста.
- Идемпотентность — свойство операции давать одинаковый результат при повторных вызовах.
Итог
Вы научились конфигурировать Jest и SuperTest для тестирования простого Express REST API, писать тесты для CRUD-эндпойнтов и применять практики подготовки окружения и очистки данных. Следуйте чек-листам, автоматизируйте тесты в CI и переключайтесь на тестовую БД для более реалистичных интеграционных проверок.
Важное: начните с простых позитивных тестов и постепенно добавляйте негативные сценарии и стресс-тесты.
Ключевые ссылки в материале: экспорт app для SuperTest, использование express.json(), использование crypto.randomUUID() для уникальных id.
Похожие материалы
Добавить текстуру к тексту в Photoshop
Как очистить и продезинфицировать пульт от телевизора
Включить NumLock при запуске Windows 10
Включить Emoji 15 на Windows 11
Unity Lights: циферблат Apple Watch для Black History Month