Какие виды тестов ты применяешь в 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-тестов проверяют основные пользовательские сценарии. Это дает уверенность при рефакторинге и непрерывной поставке.

Ответ 18+ 🔞

А, ну вот, тема-то какая, ебать мои старые костыли! Слушай, про пирамиду тестирования в Node.js. Это ж как раз про то, как не сойти с ума и не ждать час, пока твои тесты прогуляются по всем кустам. Я тебе сейчас на пальцах, но с кодом, объясню, как у меня это устроено.

1. Модульные тесты (Unit) — это фундамент, ёпта.

  • Зачем: Чтобы проверить одну маленькую функцию, как она сама по себе работает. Без всяких приключений с базой данных или интернетами.
  • Чем бью: В основном Jest, потому что он из коробки всё умеет. Ну или Mocha с Chai, если ты любитель старых добрых сборок.
  • Как делаю: Всё, что не сама функция — нахер мокаю. Базу, API, файлы — всё в труху. jest.mock() мой лучший друг в этом деле.

    // Тестируем сервисный слой, мокаем модуль работы с БД
    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. Интеграционные тесты — где начинается магия, а иногда и пиздец.

  • Зачем: Убедиться, что твой роут, сервис и репозиторий не поссорились и работают вместе, как одна семья (ну или хотя бы не дерутся).
  • Чем бью: Тот же Jest, но уже с Supertest, чтобы по API стучать.
  • Как делаю: Поднимаю тестовую базу, обычно в Docker, чтобы она была одна и никому не мешала. И после каждого теста её вычищаю, чтобы тесты друг другу не пакостили.

    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 тоже норм, но я за первый.
  • Как делаю: Поднимаю ВСЁ приложение (фронт и бэк) в тестовом окружении и заставляю бездушный браузер тыкать в кнопки.
    // Пример с 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, как будто там черная пятница. Смотрю, не накроется ли он медным тазом при тысяче запросов в секунду.
  • Тесты на утечки памяти: Пишу скрипт, который создаёт бардак, и смотрю в Chrome DevTools, не превращается ли мой процесс Node.js в смерда пердящего, который жрёт память без остановки.

Итоговая стратегия, чтобы не охуеть: Пишу дохуя быстрых модульных тестов на всю логику. Добавляю интеграционные на самые важные маршруты — чтобы быть уверенным, что части между собой дружат. И гордо венчаю эту пирамиду парой-тройкой ключевых E2E-сценариев, которые проверяют, что пользователь в принципе может купить у меня этот долбаный товар. Так и живу.