Unit тесты

Unit-тест — автоматический тест одной изолированной единицы кода (функции, метода, класса) без реальных внешних зависимостей.

Зачем нужно

Unit-тесты — самый быстрый и дешёвый вид тестов: запускаются за секунды, дают мгновенную обратную связь. Изоляция от БД, сети и файловой системы гарантирует воспроизводимость и детерминированность результата. Хорошие unit-тесты служат живой документацией — они объясняют, что именно делает функция и какие граничные случаи предусмотрены.

Где используется

  • Чистая бизнес-логика: расчёты, валидация, трансформации данных
  • Утилитарные функции: форматирование дат, парсинг строк, вычисления
  • Классы и сервисы с инжектируемыми зависимостями (mock вместо реального DB-клиента)
  • При TDD — unit-тест пишется до реализации функции
  • Фикс багов: тест воспроизводит баг, затем баг фиксируется

Основной контент

Структура unit-теста

// validators.js
function isEmail(str) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str);
}
module.exports = { isEmail };
// validators.test.js
const { isEmail } = require('./validators');

describe('isEmail', () => {
  // Позитивные случаи
  test('принимает корректный email', () => {
    expect(isEmail('user@example.com')).toBe(true);
  });

  test('принимает email с поддоменом', () => {
    expect(isEmail('user@mail.example.com')).toBe(true);
  });

  // Негативные случаи
  test('отклоняет строку без @', () => {
    expect(isEmail('userexample.com')).toBe(false);
  });

  test('отклоняет пустую строку', () => {
    expect(isEmail('')).toBe(false);
  });

  test('отклоняет null', () => {
    expect(isEmail(null)).toBe(false);
  });
});

Изоляция через mock-зависимости

// userService.js
class UserService {
  constructor(db) {
    this.db = db;
  }

  async getUserName(id) {
    const user = await this.db.findById(id);
    return user ? user.name.trim() : null;
  }
}
module.exports = UserService;
// userService.test.js
const UserService = require('./userService');

describe('UserService.getUserName', () => {
  test('возвращает имя с обрезанными пробелами', async () => {
    // Arrange — mock вместо реальной БД
    const mockDb = {
      findById: jest.fn.mockResolvedValue({ name: '  Alice  ' }),
    };
    const service = new UserService(mockDb);

    // Act
    const result = await service.getUserName(42);

    // Assert
    expect(result).toBe('Alice');
    expect(mockDb.findById).toHaveBeenCalledWith(42);
  });

  test('возвращает null если пользователь не найден', async () => {
    const mockDb = {
      findById: jest.fn.mockResolvedValue(null),
    };
    const service = new UserService(mockDb);

    const result = await service.getUserName(999);

    expect(result).toBeNull();
  });
});

Полезные матчеры Jest

expect(value).toBe(42);              // строгое равенство ===
expect(obj).toEqual({ a: 1 });       // глубокое равенство
expect(arr).toHaveLength(3);         // длина массива
expect(str).toContain('hello');      // подстрока
expect(fn).toThrow('error message'); // функция бросает ошибку
expect(mock).toHaveBeenCalledTimes(2);
expect(mock).toHaveBeenCalledWith('arg');
expect(value).toBeNull();
expect(value).toBeDefined();
expect(num).toBeGreaterThan(0);

Настройка и очистка

describe('Calculator', () => {
  let calc;

  beforeEach(() => {
    calc = new Calculator(); // свежий экземпляр перед каждым тестом
  });

  afterEach(() => {
    jest.clearAllMocks; // сброс mock-вызовов
  });

  test('складывает числа', () => {
    expect(calc.add(2, 3)).toBe(5);
  });
});

Частые ошибки

  • Тестировать несколько вещей в одном тесте — один test = одно утверждение; при падении сразу ясно что сломалось
  • Не изолировать зависимости — unit-тест, стучащийся в реальную БД или HTTP — это integration-тест, он медленный и нестабильный
  • Хрупкие тесты на реализациюexpect(fn.toString()).toContain('if') сломается при любом рефакторинге
  • Копипаста setup-кода — используй beforeEach для общей подготовки
  • Игнорировать async/await — если функция асинхронная, тест должен быть async и возвращать промис

Связанные темы

Ресурсы