Тестирование в JavaScript: Unit, Integration, E2E на продвинутом уровне

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Почему продвинутое тестирование — это 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                

Блоги

Рекомендаций по книгам