Jest - это test runner, библиотека проверок, мок-фреймворк и инструмент покрытия в одном пакете. Runner запускает файлы тестов, assertion library даёт expect для проверок, мок-фреймворк изолирует зависимости, а покрытие показывает исполненные строки. Для чистого JavaScript хватает самого Jest, а DOM, TypeScript и React добавляют осознанно. Сквозной пример - сервис заказа с calculateDelivery и, позже, корзина с кнопкой.
Естественная реакция новичка - запустить генератор и принять результат как есть. Команда npm init jest@latest создаёт рабочий config, а туториалы советуют копировать чужой jest.config. Соблазн понятен: тесты идут сразу. Но проблема всплывает позже - когда тест падает не из-за кода, а из-за опции, которую никто в команде не может объяснить. Config без понимания - это чёрный ящик в основании инструмента доверия.
Начните с минимума: пары dev-зависимостей и трёх скриптов хватает на весь цикл. Локально запускают npm test, для разработки удобен watch, пересобирающий изменённое, а в CI нужен отдельный скрипт без watch - с флагом --ci и покрытием. Разделение важно: watch следит за файлами и держит процесс живым - в CI это повесило бы пайплайн.
Первый тест - самый маленький честный контракт. Функция test принимает имя и тело, внутри вызывают expect с фактическим значением и матчер, описывающий ожидание. Матчер toBe сравнивает значения через Object.is и подходит для примитивов - чисел, строк, булевых. Имя теста - утверждение о поведении, а не пересказ кода: 'складывает два числа' читается как спецификация.
Теперь config, который можно объяснить построчно. Держите его в jest.config.ts и типизируйте импортом Config - редактор подскажет поля. Одна оговорка про TypeScript-конфиг: сам Jest его не прочитает, ему нужен загрузчик - по умолчанию ts-node, поэтому он и стоит в установке рядом с jest. Альтернатива - докблок @jest-config-loader esbuild-register первой строкой файла, тогда вместо ts-node ставят esbuild-register. Первое поле - testEnvironment, глобальная среда исполнения теста. Значение node даёт голый JavaScript без DOM, jsdom эмулирует браузерные API в памяти. Важно: jsdom - эмуляция, а не настоящий браузер, реального рендеринга и layout там нет.
Пройдём остальные поля. testMatch задаёт, какие файлы считать тестами, а токен <rootDir> привязывает поиск к корню проекта. Пара clearMocks и restoreMocks держит чистоту между тестами: clearMocks обнуляет историю вызовов перед каждым тестом, restoreMocks возвращает подменённым функциям исходную реализацию. collectCoverageFrom очерчивает знаменатель покрытия - файлы учитываются, даже если их не коснулся ни один тест, а восклицательный знак исключает генерируемый код и типы.
| Опция | Зачем | Не путать |
|---|---|---|
| testEnvironment | Глобальная среда node/jsdom | Не выполняет настоящий браузер |
| setupFiles | До установки test framework | Для env/полифилов |
| setupFilesAfterEnv | После expect/хуков | Для jest-dom и общих hooks |
| moduleNameMapper | Алиасы и не-JS импорты | Порядок regex имеет значение |
| transform | Компиляция TS/JSX | Jest не использует ваш bundler автоматически |
Три поля пригодятся позже, но их роль стоит понять сразу. setupFiles исполняется до установки тестового фреймворка - место для полифилов и переменных среды. setupFilesAfterEnv исполняется, когда уже есть expect и хуки, - для jest-dom и общих beforeEach. moduleNameMapper переписывает импорты: решает алиасы путей и подменяет не-JS импорты заглушкой, и порядок регулярных выражений важен. transform отвечает за компиляцию TypeScript и JSX.
Здесь и лежит корневая мысль: Jest не использует ваш сборщик автоматически. Vite или webpack собирают приложение, но Jest исполняет тесты в Node и о бандлере ничего не знает - без строки transform он встретит TypeScript и упадёт с ошибкой unexpected token. Цена явного config - несколько строк, которые нужно понимать; выгода - предсказуемость и умение чинить сборку тестов, не гадая. Типичный сбой: тест зелёный локально и красный в CI, потому что moduleNameMapper сработал в неверном порядке. Config, который вы можете объяснить, снимает целый класс таких загадок.
npm i -D jest ts-node
// package.json
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:ci": "jest --ci --coverage"
}
}import { sum } from './sum.js'
test('складывает два числа', () => {
expect(sum(2, 3)).toBe(5)
})import type { Config } from 'jest'
const config: Config = {
testEnvironment: 'node',
testMatch: ['<rootDir>/src/**/*.test.{ts,tsx}'],
clearMocks: true,
restoreMocks: true,
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/generated/**',
],
}
export default config