Jest — фреймворк для тестов на JavaScript, который ставится одной зависимостью и работает без браузера. Он даёт раннер, матчеры, моки, покрытие и снепшоты в одном пакете. Но именно из-за принципа «всё включено» новички тонут в конфигурации и в странных ошибках про ESM. Разберём рабочий минимум: от установки до покрытия в CI.
Установка и инициализация
mkdir shop && cd shop
npm init -y
npm install --save-dev jest
npx jest --init
Мастер задаст несколько вопросов: окружение (Node или jsdom), покрытие, очистка моков. Файл jest.config.js можно написать руками — он читается понятнее, чем десяток ключей в package.json. Добавьте скрипт "test": "jest", чтобы команда была одинаковой локально и в CI.
Ветка Jest 30 — актуальная; в старых проектах встречается 29, и большинство команд совпадает. Проверить версию: npx jest --version.
Конфигурация, которая действительно нужна
module.exports = {
testEnvironment: 'node',
testMatch: ['**/__tests__/**/*.test.js'],
clearMocks: true,
restoreMocks: true,
collectCoverageFrom: ['src/**/*.js'],
coverageThreshold: { global: { lines: 80, branches: 70 } },
};
testEnvironment определяет глобальные объекты: в node нет document, в jsdom есть поддельный DOM. Для тестов React и работы с DOM ставьте jest-environment-jsdom — с версии 28 он не входит в основной пакет. clearMocks и restoreMocks снимают классическую проблему «тест проходит один, но падает в наборе»: состояние моков не перетекает между тестами.
Первый тест и матчеры
Тест состоит из describe, it и проверок expect. Имя должно описывать поведение, а не функцию: «возвращает скидку для повторного заказа» лучше, чем «тест calculate».
const { calcTotal } = require('../src/cart');
describe('calcTotal', () => {
it('суммирует позиции и применяет скидку', () => {
const items = [{ price: 100, qty: 2 }, { price: 50, qty: 1 }];
expect(calcTotal(items, 0.1)).toBe(225);
});
it('возвращает 0 для пустой корзины', () => {
expect(calcTotal([], 0.1)).toBe(0);
});
});
toBe сравнивает примитивы, toEqual — структуры, toStrictEqual дополнительно проверяет типы и отсутствие лишних полей. Для объектов используйте toMatchObject, для исключений — toThrow(/текст/). Не смешивайте toBe с объектами: он сравнивает ссылки и даст ложное падение.
Для однотипных проверок используйте test.each: он печатает входные данные в имени теста и не плодит копипасту. Подготовку и очистку выносите в beforeEach и afterEach, а общие данные создавайте фабрикой, чтобы тесты не меняли один и тот же объект.
Моки и шпионы
Тест не должен ходить в сеть или в базу. Подменяйте границы модуля: функцию, HTTP-клиент, модуль целиком.
const mailer = require('../src/mailer');
jest.mock('../src/mailer');
test('отправляет письмо после регистрации', async () => {
mailer.send.mockResolvedValue({ ok: true });
await register({ email: 'user@example.org' });
expect(mailer.send).toHaveBeenCalledWith(
expect.objectContaining({ to: 'user@example.org' })
);
});
jest.mock поднимается в начало файла, поэтому переменные внутри фабрики должны начинаться с mock — иначе Jest выдаст ошибку про область видимости. Для точечной подмены одного метода есть jest.spyOn(object, 'method') и mockRestore(). Общие заглушки кладут в каталог __mocks__ рядом с модулем.
Асинхронность и таймеры
Асинхронный тест обязан вернуть промис или использовать async/await. Забытый await — самая частая причина «зелёного» теста, который ничего не проверил.
test('загружает заказ', async () => {
await expect(fetchOrder(1)).resolves.toMatchObject({ id: 1 });
await expect(fetchOrder(-1)).rejects.toThrow('not found');
});
Для setTimeout и Date включайте фейковые таймеры: jest.useFakeTimers(), затем jest.advanceTimersByTime(1000). Время можно зафиксировать через jest.setSystemTime(new Date('2026-01-01')). Не забудьте вернуть реальные таймеры в afterEach, иначе следующие тесты будут висеть.
TypeScript, ESM и DOM
Для TypeScript удобен ts-jest: он проверяет типы и транспилирует файлы. Альтернатива — @swc/jest, который быстрее, но не проверяет типы.
npm install --save-dev ts-jest @types/jest typescript
npx ts-jest config:init
npx jest --listTests
NODE_OPTIONS=--experimental-vm-modules npx jest
В конфиге для ts-jest укажите preset: 'ts-jest' и testEnvironment: 'node'. Для нативного ESM нужен флаг --experimental-vm-modules и extensionsToTreatAsEsm; проще не смешивать CommonJS и ESM в одном проекте. Для компонентов добавьте @testing-library/dom и jest-environment-jsdom, но помните: jsdom не считает раскладку и не отрисовывает страницу — проверяйте поведение, а не пиксели.
Покрытие и CI
npx jest --coverage
npx jest --ci --runInBand --coverage
npx jest --watch
npx jest -t 'скидк' -u
--ci запрещает интерактивные обновления снепшотов, --runInBand запускает тесты последовательно — полезно в контейнерах с малым объёмом памяти. -t фильтрует по имени, -u обновляет снепшоты. Порог покрытия в конфиге превращает падение качества в падение сборки, а не в устное замечание на ревью.
Снепшоты держите небольшими: огромный дамп HTML невозможно осмысленно ревьюить, и его обновляют не глядя. Для компонентов лучше явные проверки текста и ролей.
Типичные ошибки и диагностика
- «Cannot use import statement outside a module» — файл с ESM попал в CommonJS-окружение. Проверьте
typeвpackage.jsonи трансформацию. - «Jest did not exit one second after…» — остался открытый таймер, сокет или подписка. Запустите с
--detectOpenHandles, найдите источник;--forceExitмаскирует проблему. - «ReferenceError: fetch is not defined» — окружение не содержит глобального
fetch. Добавьте полифил вsetupFilesили обновите Node. - Тест проходит отдельно и падает в наборе — общее состояние, неочищенные моки или
jest.setSystemTimeбез сброса.
node --inspect-brk node_modules/.bin/jest --runInBand
npx jest --listTests
npx jest --showConfig | head -40
npx jest --detectOpenHandles
Отладчик останавливается на первой строке; дальше открывайте chrome://inspect. --listTests показывает, какие файлы Jest вообще видит — это быстрее, чем гадать, почему новый тест не запускается. --showConfig печатает итоговую конфигурацию после слияния всех источников.
Итог
Jest хорош, когда проект не требует сложной сборки: конфиг на десять строк, понятные моки и покрытие из коробки. Начните с окружения node, добавьте clearMocks, напишите тесты на чистые функции, затем переходите к интеграционным. ESM и TypeScript подключайте осознанно, а не копированием конфига из чужого репозитория. И не гонитесь за процентом покрытия: тест, который ничего не проверяет, хуже отсутствующего.