Введение: Почему продвинутое тестирование — это must-have для JavaScript-проекта
В современной разработке на Node.js тестирование перестало быть опциональным этапом. Без качественного покрытия кода тестами любой рефакторинг или добавление новой функциональности рискуют превратиться в "русскую рулетку". Однако просто написать несколько assert'ов недостаточно. На продвинутом уровне разработчик должен владеть тремя основными слоями: Unit-тесты (проверка изолированных модулей), Integration-тесты (взаимодействие компонентов, БД, API) и E2E-тесты (симуляция действий пользователя). В этой статье мы разберем практические приемы для каждого слоя на примере Node.js с использованием современных инструментов: Jest, Supertest и Playwright.
1. Unit-тестирование: изоляция, моки и стабы на продвинутом уровне
Unit-тесты проверяют работу одной функции или модуля в изоляции от внешних зависимостей (БД, файловая система, сетевые запросы). Ключевая задача — правильно замокать эти зависимости, чтобы тест был быстрым и детерминированным.
Продвинутые техники мокирования в Jest
Рассмотрим пример: функция, которая обращается к внешнему API и кэширует результат в Redis. Нам нужно протестировать логику кэширования, не выполняя реальных запросов.
// cacheService.js
const axios = require('axios');
const redis = require('redis');
class CacheService {
constructor() {
this.client = redis.createClient();
}
async fetchData(url) {
const cached = await this.client.get(url);
if (cached) return JSON.parse(cached);
const response = await axios.get(url);
await this.client.setEx(url, 3600, JSON.stringify(response.data));
return response.data;
}
}
module.exports = CacheService;
// cacheService.test.js
const CacheService = require('./cacheService');
const axios = require('axios');
const redis = require('redis');
jest.mock('axios');
jest.mock('redis', () => ({
createClient: jest.fn(() => ({
get: jest.fn(),
setEx: jest.fn()
}))
}));
describe('CacheService', () => {
let service;
let mockRedisClient;
beforeEach(() => {
mockRedisClient = redis.createClient();
service = new CacheService();
});
test('should return cached data if present', async () => {
const url = 'https://api.example.com/data';
const cachedData = { id: 1, value: 'cached' };
mockRedisClient.get.mockResolvedValue(JSON.stringify(cachedData));
const result = await service.fetchData(url);
expect(result).toEqual(cachedData);
expect(axios.get).not.toHaveBeenCalled();
});
test('should fetch and cache data if not cached', async () => {
const url = 'https://api.example.com/data';
const apiData = { id: 2, value: 'fresh' };
mockRedisClient.get.mockResolvedValue(null);
axios.get.mockResolvedValue({ data: apiData });
const result = await service.fetchData(url);
expect(result).toEqual(apiData);
expect(mockRedisClient.setEx).toHaveBeenCalledWith(url, 3600, JSON.stringify(apiData));
});
});
Ключевые моменты:
jest.mock('axios')автоматически замокает весь модуль axios.- Для Redis мы используем фабрику, возвращающую мок с методами
getиsetEx. - Проверяем, что при наличии кэша внешний вызов не происходит.
2. Integration-тесты: тестируем взаимодействие с реальной БД и API
Integration-тесты проверяют, как разные части системы работают вместе. Для Node.js это часто означает тестирование Express-маршрутов с реальной (или тестовой) базой данных. Лучший инструмент — Supertest в паре с Jest.
Настройка тестовой БД и Supertest
Пример: API для управления пользователями. Используем SQLite в памяти для изоляции тестов.
// userRoutes.js
const express = require('express');
const router = express.Router();
const db = require('./db');
router.post('/users', async (req, res) => {
try {
const { name, email } = req.body;
const result = await db.run(
'INSERT INTO users (name, email) VALUES (?, ?)',
[name, email]
);
res.status(201).json({ id: result.lastID, name, email });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
module.exports = router;
// userRoutes.test.js
const request = require('supertest');
const express = require('express');
const userRoutes = require('./userRoutes');
const db = require('./db');
// Инициализация тестовой БД
beforeAll(async () => {
await db.run('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)');
});
afterEach(async () => {
await db.run('DELETE FROM users');
});
afterAll(async () => {
await db.close();
});
const app = express();
app.use(express.json());
app.use('/api', userRoutes);
describe('POST /api/users', () => {
test('should create a new user', async () => {
const newUser = { name: 'Alice', email: 'alice@example.com' };
const response = await request(app)
.post('/api/users')
.send(newUser)
.expect(201);
expect(response.body).toMatchObject(newUser);
expect(response.body.id).toBeDefined();
});
test('should return 500 if email is missing', async () => {
await request(app)
.post('/api/users')
.send({ name: 'Bob' })
.expect(500);
});
});
Продвинутые советы:
- Обязательно очищайте БД между тестами (
afterEach), чтобы избежать "липких" данных. - Используйте транзакции с откатом для ускорения тестов.
- Для внешних API (например, платежного шлюза) используйте библиотеки типа
nockилиmsw.
3. E2E-тестирование с Playwright: от клика до проверки UI
End-to-End тесты симулируют поведение реального пользователя в браузере. Для Node.js проектов (особенно если у вас есть фронтенд на React/Vue/Angular) Playwright — лучший выбор: он быстрее Puppeteer, поддерживает несколько браузеров и умеет ждать элементы.
Сценарий: авторизация и проверка профиля
Допустим, у нас есть SPA с формой логина и страницей профиля.
// login.e2e.js
const { test, expect } = require('@playwright/test');
test.describe('User