Какие виды тестов ты применяешь в Node.js-проектах?

«Какие виды тестов ты применяешь в Node.js-проектах?» — вопрос из категории Тестирование, который задают на 26% собеседований Node.js Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

В своих Node.js-проектах я выстраиваю пирамиду тестирования, чтобы балансировать между скоростью выполнения и надежностью.

1. Модульные тесты (Unit) — основа пирамиды.

  • Цель: Проверить изолированно отдельную функцию, модуль или класс.
  • Инструменты: Jest (основной выбор) или Mocha + Chai.
  • Практика: Мокаю все внешние зависимости (запросы к БД, вызовы API, файловую систему) с помощью jest.mock() или библиотек вроде sinon.

    // Тестируем сервисный слой, мокаем модуль работы с БД
    jest.mock('../db/userRepository');
    const { getUserById } = require('../db/userRepository');
    const { getUserProfile } = require('../services/userService');
    
    test('getUserProfile returns formatted user data', async () => {
        // Подготовка мока
        getUserById.mockResolvedValue({ id: 1, name: 'Alice', email: 'alice@example.com' });
    
        // Вызов тестируемой функции
        const result = await getUserProfile(1);
    
        // Проверки
        expect(result).toHaveProperty('displayName', 'Alice');
        expect(getUserById).toHaveBeenCalledWith(1);
    });

2. Интеграционные тесты.

  • Цель: Проверить взаимодействие нескольких модулей (например, Express-роут + сервис + репозиторий).
  • Инструменты: Jest, Supertest для тестирования HTTP API.
  • Практика: Поднимаю тестовую БД (часто в Docker-контейнере или используя sqlite:memory: для SQL) и очищаю её перед каждым тестом.

    const request = require('supertest');
    const app = require('../app'); // Express app
    const db = require('../db');
    
    beforeAll(async () => await db.migrate.latest());
    afterEach(async () => await db('users').truncate());
    
    test('POST /api/users creates a new user', async () => {
        const res = await request(app)
            .post('/api/users')
            .send({ name: 'Bob' });
    
        expect(res.statusCode).toBe(201);
        expect(res.body).toHaveProperty('id');
    
        // Проверяем, что данные реально попали в БД
        const dbUser = await db('users').where({ id: res.body.id }).first();
        expect(dbUser.name).toBe('Bob');
    });

3. E2E (сквозные) тесты.

  • Цель: Имитировать поведение реального пользователя в максимально приближенной к продакшену среде.
  • Инструменты: Playwright (мой фаворит) или Cypress.
  • Практика: Запускаю всё приложение (frontend + backend) в тестовом окружении и автоматизирую сценарии через браузер.
    // Пример с Playwright
    test('user can complete checkout flow', async ({ page }) => {
        await page.goto('/products/1');
        await page.click('button:has-text("Add to Cart")');
        await page.goto('/cart');
        await expect(page.locator('.cart-item')).toHaveCount(1);
        await page.click('button:has-text("Checkout")');
        // ... заполнение формы, оплата
        await expect(page).toHaveURL('/order/confirmed');
    });

Дополнительные виды:

  • Нагрузочные тесты: Использую k6 для проверки, как API держит нагрузку в 100-1000 RPS.
  • Тесты на утечки памяти: Пишу скрипты, которые создают нагрузку, и слежу за памятью процесса Node.js с помощью --inspect и Chrome DevTools.

Стратегия: Пишу много быстрых модульных тестов, покрывающих бизнес-логику. Интеграционные тесты покрывают критические API-маршруты. Несколько ключевых E2E-тестов проверяют основные пользовательские сценарии. Это дает уверенность при рефакторинге и непрерывной поставке.