Тестирование в JavaScript: Unit, Integration, E2E — от А до Я
Разработка современного веб-приложения на JavaScript (особенно на Node.js) невозможна без качественного тестирования. Тесты не только ловят баги до попадания в продакшн, но и служат живой документацией, а также позволяют рефакторить код без страха что-то сломать. В этой статье мы разберём три основных уровня пирамиды тестирования: unit (модульные), integration (интеграционные) и e2e (сквозные). Вы узнаете, чем они отличаются, какие инструменты использовать и увидите готовые примеры на Node.js.
1. Unit-тесты: тестируем изолированные модули
Unit-тесты — это фундамент пирамиды. Они проверяют работу отдельных функций, классов или компонентов в полной изоляции от внешних зависимостей (БД, API, файловой системы). Основная цель — убедиться, что логика модуля работает корректно при любых входных данных.
Для Node.js самым популярным фреймворком является Jest (от Facebook). Он не требует сложной конфигурации, имеет встроенный ассершн, моки и покрытие кода.
Пример: тестируем функцию сортировки массива
// utils.js
function sortNumbers(arr) {
if (!Array.isArray(arr)) {
throw new Error('Input must be an array');
}
return [...arr].sort((a, b) => a - b);
}
module.exports = { sortNumbers };
// utils.test.js
const { sortNumbers } = require('./utils');
describe('sortNumbers', () => {
test('should sort an array of numbers in ascending order', () => {
expect(sortNumbers([3, 1, 2])).toEqual([1, 2, 3]);
});
test('should handle an empty array', () => {
expect(sortNumbers([])).toEqual([]);
});
test('should throw error for non-array input', () => {
expect(() => sortNumbers('string')).toThrow('Input must be an array');
});
});
Best practices для unit-тестов:
- Один тест — одна проверка (один assert).
- Используйте
describeдля группировки тестов по функциям. - Мокайте внешние зависимости (с помощью
jest.mock()). - Пишите тесты до написания кода (TDD — Test-Driven Development).
2. Интеграционные тесты: проверяем взаимодействие компонентов
Интеграционные тесты проверяют, как различные модули системы работают вместе. Например, как ваш контроллер взаимодействует с базой данных или внешним API. В отличие от unit-тестов, здесь мы не мокаем всё подряд, а используем реальные (или тестовые) окружения.
Для тестирования HTTP-серверов на Express отлично подходит библиотека Supertest в паре с Jest.
Пример: тестируем REST-эндпоинт
// app.js
const express = require('express');
const app = express();
app.get('/api/hello', (req, res) => {
res.json({ message: 'Hello, World!' });
});
app.post('/api/data', (req, res) => {
const { name } = req.body;
if (!name) {
return res.status(400).json({ error: 'Name is required' });
}
res.status(201).json({ received: name });
});
module.exports = app;
// app.test.js
const request = require('supertest');
const app = require('./app');
describe('Integration tests for API', () => {
test('GET /api/hello should return a greeting', async () => {
const response = await request(app)
.get('/api/hello')
.expect(200);
expect(response.body).toEqual({ message: 'Hello, World!' });
});
test('POST /api/data with valid body should return 201', async () => {
const response = await request(app)
.post('/api/data')
.send({ name: 'Alice' })
.expect(201);
expect(response.body).toEqual({ received: 'Alice' });
});
test('POST /api/data without name should return 400', async () => {
const response = await request(app)
.post('/api/data')
.send({})
.expect(400);
expect(response.body).toEqual({ error: 'Name is required' });
});
});
Советы по интеграционным тестам:
- Используйте тестовую БД (например, SQLite в памяти или отдельный контейнер Docker).
- Очищайте данные между тестами (хуки
beforeEach/afterEach). - Тестируйте не только happy path, но и ошибки (400, 404, 500).
3. E2E-тесты: симуляция действий пользователя
Сквозные (End-to-End) тесты — это вершина пирамиды. Они эмулируют поведение реального пользователя: открывают браузер, кликают по кнопкам, заполняют формы, проверяют, что данные отображаются корректно. E2E-тесты дают наибольшую уверенность, но они самые медленные и хрупкие.
Для Node.js часто используют Puppeteer (управление Chrome) или Playwright (более современный, поддерживает несколько браузеров).
Пример: проверка формы входа
// login.e2e.test.js
const puppeteer = require('puppeteer');
let browser;
let page;
beforeAll(async () => {
browser = await puppeteer.launch({ headless: true });
page = await browser.newPage();
});
afterAll(async () => {
await browser.close();
});
test('should display error for invalid login', async () => {
await page.goto('http://localhost:3000/login');
// Заполняем форму
await page.type('#username', 'wrong_user');
await page.type('#password', 'wrong_pass');
await page.click('button[type="submit"]');
// Ждём появления сообщения об ошибке
await page.waitForSelector('.error-message', { timeout: 5000 });
const errorText = await page.$eval('.error-message', el => el.textContent);
expect(errorText).toContain('Invalid credentials');
});
Когда использовать E2E?
- Для критических пользовательских сценариев (регистрация, оплата).
- Для smoke-тестов после деплоя.
- Когда нужно проверить взаимодействие фронтенда и бэкенда в реальном браузере.
Важно: Не пытайтесь покрыть E2E-тестами всё приложение — это дорого и медленно. Доста