Как было организовано тестирование в вашем Node.js-проекте?

Ответ

Мы использовали комбинацию разных уровней тестирования с Jest в качестве основного фреймворка.

1. Unit-тесты: Тестировали отдельные функции, модули и классы, изолируя их от внешних зависимостей с помощью моков (jest.mock()).

// service/userService.test.js
const userService = require('./userService');
const User = require('../models/User');

jest.mock('../models/User'); // Мокаем модель Mongoose

describe('User Service', () => {
  test('createUser should hash password', async () => {
    User.mockImplementation(() => ({
      save: jest.fn().mockResolvedValue({ _id: '123', email: 'test@mail.com' })
    }));

    const result = await userService.createUser('test@mail.com', 'plainPassword');
    expect(result.email).toBe('test@mail.com');
    expect(result.password).not.toBe('plainPassword'); // Проверяем, что пароль захэширован
  });
});

2. Интеграционные тесты: Проверяли взаимодействие нескольких модулей, например, сервиса с реальной (или in-memory) базой данных. Для этого поднимали тестовый экземпляр MongoDB с помощью mongodb-memory-server.

3. API (E2E) тесты: С помощью Supertest тестировали HTTP-эндпоинты нашего Express-приложения.

// tests/api/users.test.js
const request = require('supertest');
const app = require('../../app');
const { generateAuthToken } = require('../../utils/auth');

describe('GET /api/users', () => {
  test('should return 401 without token', async () => {
    const res = await request(app).get('/api/users');
    expect(res.statusCode).toBe(401);
  });

  test('should return user list for admin', async () => {
    const adminToken = generateAuthToken({ userId: '1', role: 'admin' });
    const res = await request(app)
      .get('/api/users')
      .set('Authorization', `Bearer ${adminToken}`);
    expect(res.statusCode).toBe(200);
    expect(Array.isArray(res.body)).toBe(true);
  });
});

Инфраструктура и инструменты:

  • Husky + lint-staged: Запуск линтера (ESLint) и юнит-тестов перед коммитом.
  • Jest coverage: Генерация отчета о покрытии кода тестами (npm test -- --coverage).
  • GitHub Actions: Автоматический прогон всей тестовой suites при каждом пуше в PR. Деплой на production происходил только после успешного прохождения всех тестов.

Ответ 18+ 🔞

А, ну это про тесты, да? Слушай, тут история как у того мужика, который решил проверить, выдержит ли табуретка — сел, а она развалилась. Вот и мы так же подошли: покрыли всё, от мелких функций до целых API, чтобы потом не охуеть в продакшене.

1. Юнит-тесты — это как проверять каждый винтик в двигателе отдельно. Каждую функцию, каждый модуль мы тестили изолированно, от всех внешних зависимостей отъебались с помощью моков (jest.mock()). Чисто, без лишнего шума.

// service/userService.test.js
const userService = require('./userService');
const User = require('../models/User');

jest.mock('../models/User'); // Модель Mongoose просто послали нахуй, подсунули заглушку

describe('User Service', () => {
  test('createUser should hash password', async () => {
    User.mockImplementation(() => ({
      save: jest.fn().mockResolvedValue({ _id: '123', email: 'test@mail.com' })
    }));

    const result = await userService.createUser('test@mail.com', 'plainPassword');
    expect(result.email).toBe('test@mail.com');
    expect(result.password).not.toBe('plainPassword'); // Смотрим, чтобы пароль захэшировался, а не лежал голым текстом
  });
});

2. Интеграционные тесты — собираем винтики обратно и смотрим, крутится ли двигатель. Тут уже проверяли, как несколько модулей друг с другом работают. Например, сервис с реальной (ну или in-memory) базой. Для этого поднимали тестовый MongoDB через mongodb-memory-server — удобно, не надо заморачиваться с отдельным сервером.

3. API (E2E) тесты — это уже полная сборка: садимся в машину и пытаемся завестись. Тут Supertest в руки и начинаем долбить по эндпоинтам нашего Express-приложения, как будто это делает настоящий пользователь.

// tests/api/users.test.js
const request = require('supertest');
const app = require('../../app');
const { generateAuthToken } = require('../../utils/auth');

describe('GET /api/users', () => {
  test('should return 401 without token', async () => {
    const res = await request(app).get('/api/users');
    expect(res.statusCode).toBe(401); // Без токена — иди нахуй, 401
  });

  test('should return user list for admin', async () => {
    const adminToken = generateAuthToken({ userId: '1', role: 'admin' });
    const res = await request(app)
      .get('/api/users')
      .set('Authorization', `Bearer ${adminToken}`);
    expect(res.statusCode).toBe(200); // А с админским токеном — вот тебе список, получай
    expect(Array.isArray(res.body)).toBe(true);
  });
});

Инфраструктура и инструменты — чтобы не превратиться в распиздяев:

  • Husky + lint-staged: Перед каждым коммитом автоматом гоняли линтер (ESLint) и юнит-тесты. Хуйню закоммитить не давали.
  • Jest coverage: Генерировали отчёт, сколько кода покрыто тестами (npm test -- --coverage). Показатели были, конечно, не айс, но стремились.
  • GitHub Actions: Автоматический прогон ВСЕХ тестов при каждом пуше в PR. И самое главное — деплой на продакшен летел только если все тесты проходили. А иначе — сиди, чини, пока не заработает.