TL;DR

  • Jest — фреймворк тестирования без конфигурации со встроенными assertions, моками и покрытием
  • Matchers вроде toBe, toEqual, toContain делают assertions читаемыми
  • Мокай функции с jest.fn(), модули с jest.mock(), таймеры с jest.useFakeTimers()
  • Async тестирование: используй async/await, resolves/rejects или callback done
  • Snapshot тестирование захватывает UI — полезно для React компонентов

Подходит для: JavaScript/TypeScript разработчиков, React/Vue/Node.js проектов, команд, хотящих всё-в-одном Пропусти, если: Нужно браузерное тестирование (используй Playwright/Cypress) Время чтения: 15 минут

Твой тестовый набор выполняется 10 минут. Половина тестов flaky. Никто больше не доверяет результатам.

Jest меняет это. Он быстрый, надежный и работает из коробки. Без ада конфигурации. Без сборки пяти разных библиотек.

Этот туториал учит Jest с нуля — matchers, моки, async тестирование и паттерны, которые делают тесты поддерживаемыми.

Что такое Jest?

Jest — фреймворк тестирования JavaScript, созданный Facebook. Он запускает тесты, предоставляет assertions, мокает зависимости и генерирует отчеты о покрытии — всё в одном пакете.

Что включает Jest:

  • Test runner — находит и выполняет тестовые файлы
  • Библиотека assertionsexpect() со встроенными matchers
  • Моки — mock функции, модули, таймеры
  • Покрытие — встроенные отчеты о покрытии кода
  • Snapshot тестирование — захватывает и сравнивает output
  • Watch режим — перезапускает тесты при изменении файлов

Установка и настройка

Новый проект

# Инициализация проекта
npm init -y

# Установка Jest
npm install --save-dev jest

# Добавление test скрипта в package.json
npm pkg set scripts.test="jest"

TypeScript проект

npm install --save-dev jest typescript ts-jest @types/jest

# Инициализация ts-jest конфига
npx ts-jest config:init

Create React App

Jest уже включен. Просто запусти:

npm test

Структура проекта

my-project/
├── src/
│   ├── calculator.js
│   └── utils/
│       └── formatters.js
├── __tests__/
│   ├── calculator.test.js
│   └── utils/
│       └── formatters.test.js
├── jest.config.js
└── package.json

Jest находит тесты в:

  • Файлах, заканчивающихся на .test.js или .spec.js
  • Файлах в папках __tests__

Написание первого теста

// src/calculator.js
function add(a, b) {
  return a + b;
}

function divide(a, b) {
  if (b === 0) {
    throw new Error('Cannot divide by zero');
  }
  return a / b;
}

module.exports = { add, divide };
// __tests__/calculator.test.js
const { add, divide } = require('../src/calculator');

describe('Calculator', () => {
  describe('add', () => {
    test('складывает два положительных числа', () => {
      expect(add(2, 3)).toBe(5);
    });

    test('складывает отрицательные числа', () => {
      expect(add(-1, -1)).toBe(-2);
    });

    test('складывает с нулем', () => {
      expect(add(5, 0)).toBe(5);
    });
  });

  describe('divide', () => {
    test('делит два числа', () => {
      expect(divide(10, 2)).toBe(5);
    });

    test('выбрасывает ошибку при делении на ноль', () => {
      expect(() => divide(10, 0)).toThrow('Cannot divide by zero');
    });
  });
});

Запуск тестов

# Запустить все тесты
npm test

# Запустить конкретный файл
npm test -- calculator.test.js

# Запустить тесты по паттерну
npm test -- --testNamePattern="складывает"

# Watch режим
npm test -- --watch

# С покрытием
npm test -- --coverage

Matchers

Matchers — методы проверки значений. Jest имеет 50+ встроенных matchers.

Частые Matchers

// Равенство
expect(2 + 2).toBe(4);                    // Строгое равенство (===)
expect({ a: 1 }).toEqual({ a: 1 });       // Глубокое равенство

// Истинность
expect(true).toBeTruthy();
expect(false).toBeFalsy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect('value').toBeDefined();

// Числа
expect(10).toBeGreaterThan(5);
expect(10).toBeGreaterThanOrEqual(10);
expect(5).toBeLessThan(10);
expect(0.1 + 0.2).toBeCloseTo(0.3);       // Числа с плавающей точкой

// Строки
expect('Hello World').toMatch(/World/);
expect('Hello World').toContain('World');

// Массивы
expect([1, 2, 3]).toContain(2);
expect([1, 2, 3]).toHaveLength(3);
expect(['a', 'b']).toEqual(expect.arrayContaining(['a']));

// Объекты
expect({ a: 1, b: 2 }).toHaveProperty('a');
expect({ a: 1, b: 2 }).toHaveProperty('a', 1);
expect({ a: 1 }).toMatchObject({ a: 1 });

// Исключения
expect(() => { throw new Error('fail'); }).toThrow();
expect(() => { throw new Error('fail'); }).toThrow('fail');
expect(() => { throw new Error('fail'); }).toThrow(Error);

Отрицание Matchers

Добавь .not перед любым matcher:

expect(5).not.toBe(3);
expect([1, 2]).not.toContain(3);
expect({ a: 1 }).not.toHaveProperty('b');

Тестирование асинхронного кода

Async/Await

// src/api.js
async function fetchUser(id) {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) {
    throw new Error('User not found');
  }
  return response.json();
}

module.exports = { fetchUser };
// __tests__/api.test.js
test('успешно получает пользователя', async () => {
  const user = await fetchUser(1);
  expect(user.name).toBe('John');
});

test('выбрасывает ошибку для невалидного пользователя', async () => {
  await expect(fetchUser(999)).rejects.toThrow('User not found');
});

Промисы с resolves/rejects

test('резолвится в данные пользователя', () => {
  return expect(fetchUser(1)).resolves.toMatchObject({ name: 'John' });
});

test('реджектится для отсутствующего пользователя', () => {
  return expect(fetchUser(999)).rejects.toThrow();
});

Callback стиль (done)

function fetchDataWithCallback(callback) {
  setTimeout(() => {
    callback({ data: 'result' });
  }, 100);
}

test('вызывает callback с данными', (done) => {
  function callback(result) {
    expect(result.data).toBe('result');
    done();  // Тест ждет пока вызовется done()
  }
  fetchDataWithCallback(callback);
});

Моки

Моки заменяют реальные реализации контролируемыми заменителями.

Mock функции (jest.fn)

test('mock функция отслеживает вызовы', () => {
  const mockCallback = jest.fn(x => x + 1);

  [1, 2, 3].forEach(mockCallback);

  // Проверка количества вызовов
  expect(mockCallback).toHaveBeenCalledTimes(3);

  // Проверка конкретных вызовов
  expect(mockCallback).toHaveBeenCalledWith(1);
  expect(mockCallback).toHaveBeenLastCalledWith(3);

  // Проверка возвращаемых значений
  expect(mockCallback.mock.results[0].value).toBe(2);
});

Mock возвращаемые значения

const mock = jest.fn();

// Возврат разных значений при последовательных вызовах
mock
  .mockReturnValueOnce(10)
  .mockReturnValueOnce(20)
  .mockReturnValue(30);

console.log(mock()); // 10
console.log(mock()); // 20
console.log(mock()); // 30
console.log(mock()); // 30

// Mock resolved/rejected промисов
const asyncMock = jest.fn()
  .mockResolvedValueOnce({ success: true })
  .mockRejectedValueOnce(new Error('Failed'));

Мок модулей

// Мок всего модуля
jest.mock('./api');

const { fetchUser } = require('./api');

// Настройка mock реализации
fetchUser.mockResolvedValue({ id: 1, name: 'Mock User' });

test('использует замоканный API', async () => {
  const user = await fetchUser(1);
  expect(user.name).toBe('Mock User');
});

Мок с фабрикой

jest.mock('./database', () => ({
  connect: jest.fn().mockResolvedValue(true),
  query: jest.fn().mockResolvedValue([{ id: 1 }]),
  close: jest.fn()
}));

Шпионаж за методами

const video = {
  play() {
    return true;
  }
};

test('шпионаж за методом', () => {
  const spy = jest.spyOn(video, 'play');

  video.play();

  expect(spy).toHaveBeenCalled();
  expect(spy).toHaveReturnedWith(true);

  spy.mockRestore();  // Восстановить оригинальную реализацию
});

Мок таймеров

jest.useFakeTimers();

function delayedGreeting(callback) {
  setTimeout(() => callback('Hello'), 1000);
}

test('вызывает callback после задержки', () => {
  const callback = jest.fn();

  delayedGreeting(callback);

  expect(callback).not.toHaveBeenCalled();

  jest.advanceTimersByTime(1000);  // Перемотка времени

  expect(callback).toHaveBeenCalledWith('Hello');
});

// Или запустить все таймеры
test('с runAllTimers', () => {
  const callback = jest.fn();

  delayedGreeting(callback);
  jest.runAllTimers();

  expect(callback).toHaveBeenCalled();
});

Setup и Teardown

describe('Database tests', () => {
  let db;

  // Выполняется один раз перед всеми тестами в этом describe
  beforeAll(async () => {
    db = await connectToDatabase();
  });

  // Выполняется один раз после всех тестов
  afterAll(async () => {
    await db.close();
  });

  // Выполняется перед каждым тестом
  beforeEach(async () => {
    await db.clear();
  });

  // Выполняется после каждого теста
  afterEach(() => {
    jest.clearAllMocks();
  });

  test('вставляет запись', async () => {
    await db.insert({ name: 'Test' });
    const records = await db.findAll();
    expect(records).toHaveLength(1);
  });
});

Snapshot тестирование

Snapshots захватывают output и обнаруживают непреднамеренные изменения.

// src/formatUser.js
function formatUser(user) {
  return {
    displayName: `${user.firstName} ${user.lastName}`,
    email: user.email.toLowerCase(),
    initials: `${user.firstName[0]}${user.lastName[0]}`
  };
}

module.exports = { formatUser };
// __tests__/formatUser.test.js
const { formatUser } = require('../src/formatUser');

test('форматирует пользователя корректно', () => {
  const user = {
    firstName: 'John',
    lastName: 'Doe',
    email: 'John.Doe@Example.com'
  };

  expect(formatUser(user)).toMatchSnapshot();
});

Первый запуск создает __snapshots__/formatUser.test.js.snap:

exports[`форматирует пользователя корректно 1`] = `
{
  "displayName": "John Doe",
  "email": "john.doe@example.com",
  "initials": "JD"
}
`;

Если output изменится, тест упадет. Обнови snapshots:

npm test -- --updateSnapshot
# или
npm test -- -u

Покрытие кода

# Генерация отчета о покрытии
npm test -- --coverage

# С конкретными порогами
npm test -- --coverage --coverageThreshold='{"global":{"branches":80,"functions":80}}'

Конфигурация покрытия

// jest.config.js
module.exports = {
  collectCoverageFrom: [
    'src/**/*.{js,jsx,ts,tsx}',
    '!src/**/*.d.ts',
    '!src/index.js'
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    }
  },
  coverageReporters: ['text', 'lcov', 'html']
};

Лучшие практики

1. Один Assertion на тест (обычно)

// Плохо: несколько несвязанных assertions
test('валидация пользователя', () => {
  expect(isValidEmail('test@example.com')).toBe(true);
  expect(isValidEmail('invalid')).toBe(false);
  expect(isValidName('John')).toBe(true);
});

// Хорошо: отдельные тесты
test('принимает валидный email', () => {
  expect(isValidEmail('test@example.com')).toBe(true);
});

test('отклоняет невалидный email', () => {
  expect(isValidEmail('invalid')).toBe(false);
});

2. Описательные имена тестов

// Плохо
test('test1', () => { ... });

// Хорошо
test('возвращает null когда user ID не найден', () => { ... });

3. Паттерн Arrange-Act-Assert

test('вычисляет сумму со скидкой', () => {
  // Arrange
  const cart = { items: [{ price: 100 }, { price: 50 }] };
  const discount = 0.1;

  // Act
  const total = calculateTotal(cart, discount);

  // Assert
  expect(total).toBe(135);
});

4. Избегай тестирования деталей реализации

// Плохо: тестирует внутреннее состояние
test('устанавливает внутренний флаг', () => {
  const counter = new Counter();
  counter.increment();
  expect(counter._count).toBe(1);  // Тестирование приватного свойства
});

// Хорошо: тестирует публичное поведение
test('увеличивает счетчик', () => {
  const counter = new Counter();
  counter.increment();
  expect(counter.getCount()).toBe(1);  // Тестирование публичного метода
});

AI-Assisted Jest тестирование

AI инструменты могут ускорить написание тестов при правильном использовании.

Что AI делает хорошо:

  • Генерация тест-кейсов из сигнатур функций
  • Создание mock данных определенной формы
  • Написание boilerplate для частых паттернов
  • Предложение edge cases, которые ты мог пропустить

Что всё ещё требует людей:

  • Решения о том, что стоит тестировать
  • Проверка, что тесты действительно тестируют нужное
  • Написание тестов для сложной бизнес-логики
  • Отладка упавших тестов

FAQ

Для чего используется Jest?

Jest — фреймворк тестирования JavaScript для unit-тестов, интеграционных тестов и snapshot-тестирования. Он предоставляет test runner, библиотеку assertions, утилиты моков и покрытие кода — всё в одном пакете. Jest работает с React, Vue, Angular, Node.js и любым JavaScript или TypeScript проектом.

Jest только для React?

Нет. Хотя Jest был создан Facebook и популярен в React проектах, он работает с любой JavaScript или TypeScript кодовой базой. Jest одинаково хорошо тестирует Node.js бэкенд, Vue компоненты, Angular приложения и vanilla JavaScript. React Testing Library — отдельный пакет, который дополняет Jest для React-специфичного тестирования.

В чем разница между Jest и Mocha?

Jest — всё-в-одном фреймворк со встроенными assertions (expect), моками (jest.fn) и покрытием. Mocha — test runner, требующий отдельных библиотек: Chai для assertions, Sinon для моков, nyc для покрытия. Jest проще настроить; Mocha предлагает больше гибкости и кастомизации. Для новых проектов Jest обычно проще.

Как мокать API вызовы в Jest?

Несколько подходов работают:

  1. jest.mock() — мок всего fetch/axios модуля
  2. jest.spyOn() — шпионаж и мок конкретных методов
  3. Manual mocks — создание папки __mocks__ с mock реализациями
  4. MSW (Mock Service Worker) — перехват сетевых запросов для реалистичного API мока

Официальные ресурсы

Смотрите также