Jest — классическое тестирование JS
Jest — тестовый фреймворк от Meta. Он десятилетиями доминирует в экосистеме React: богатые матчеры, умелые моки, снапшоты и отчёт о покрытии — всё из коробки без особого конфига.
1. Jest и место в экосистеме
Jest — «тест-раннер с самодостаточным API»:
- не нужны отдельные библиотеки для assertions и моков;
- watch-режим перезапускает только изменённые тесты;
- встроенный coverage-отчёт (istanbul);
- testing-library — стандартный способ тестировать React.
2. Установка
npm install --save-dev jest @types/jest// package.json
"scripts": {
"test": "jest",
"test:watch": "jest --watch"
}// jest.config.js
module.exports = {
testEnvironment: "jsdom", // если тестируем DOM-компоненты
testMatch: ["**/*.test.js", "**/*.test.jsx", "**/*.test.ts", "**/*.test.tsx"]
};Jest ищет файлы *.test.js по умолчанию. Для TypeScript нужен пресет ts-jest или Babel.
3. Структура теста
// sum.js
export const sum = (a, b) => a + b;// sum.test.js
import { sum } from "./sum";
describe("sum", () => {
test("сложение двух чисел", () => {
expect(sum(2, 3)).toBe(5);
});
});test и it — синонимы. describe группирует, expect с матчером проверяет.
4. Матчеры
| Матчер | Цель |
|---|---|
toBe | примитивное равенство (===) |
toEqual | глубокое равенство объектов/массивов |
toMatchObject | совпадение части свойств |
toMatch | строка соответствует regexp |
toContain | элемент в массиве / строка в строке |
toThrow | исключение |
expect({ a: 1, b: 2 }).toEqual({ a: 1, b: 2 });
expect("Hello").toMatch(/ell/);
expect([1, 2, 3]).toContain(2);
expect(() => JSON.parse("no")).toThrow();toBeCloseTo — избегаешь сюрпризов точности.5. Моки: jest.fn и jest.spyOn
// mock-функция
const cb = jest.fn();
cb(1, 2);
expect(cb).toHaveBeenCalled();
expect(cb).toHaveBeenCalledWith(1, 2);
expect(cb).toHaveBeenCalledTimes(1);
// реализация по умолчанию
const add = jest.fn().mockImplementation((a, b) => a + b);
expect(add(2, 3)).toBe(5);// spy на существующий объект
const math = { multiply: (a, b) => a * b };
const spy = jest.spyOn(math, "multiply");
math.multiply(2, 4);
expect(spy).toHaveBeenCalledWith(2, 4);
spy.mockRestore();6. Мок модулей: jest.mock
Заменяем целый модуль, чтобы изолировать тестируемый код от сети:
// api.js
export const getUsers = () => fetch("/users").then(r => r.json());// users.js
import { getUsers } from "./api";
export const loadCount = async () => {
const users = await getUsers();
return users.length;
};// users.test.js
jest.mock("./api", () => ({
getUsers: jest.fn(async () => [{ id: 1 }, { id: 2 }])
}));
import { loadCount } from "./users";
test("loadCount возвращает длину списка", async () => {
const count = await loadCount();
expect(count).toBe(2);
});jest.spyOn(global, "fetch") и подменяй возврат: для этого и нужен jsdom-environment.7. Fake timers
Тесты с setTimeout/setInterval не должны ждать реальных секунд:
jest.useFakeTimers();
const sayHello = (cb) => setTimeout(() => cb("hello"), 1000);
test("вызывает колбэк через секунду", () => {
const cb = jest.fn();
sayHello(cb);
expect(cb).not.toHaveBeenCalled();
jest.advanceTimersByTime(1000); // «проматываем» секунду
expect(cb).toHaveBeenCalledWith("hello");
});
afterEach(() => jest.useRealTimers());8. Тесты React: Testing Library
npm install --save-dev @testing-library/react @testing-library/jest-dom jest-environment-jsdom// Counter.tsx
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
);
}// Counter.test.tsx
import { render, screen, fireEvent } from "@testing-library/react";
import { Counter } from "./Counter";
test("клик по кнопке увеличивает счёт", () => {
render(<Counter />);
const btn = screen.getByRole("button");
fireEvent.click(btn);
expect(btn.textContent).toBe("1");
});9. Снапшоты (snapshots)
Снапшот «фотографирует» разметку компонента:
test("снапшот кнопки", () => {
const { asFragment } = render(<Counter />);
expect(asFragment()).toMatchSnapshot();
});При первом запуске создаётся файл __snapshots__/...snap. При следующих — сравнение с ним. Если изменения осознанные — обновляй флагом jest -u.
10. Покрытие кода
jest --coverageВ консоли появится таблица: Statements, Branches, Functions, Lines. Порог можно задать в конфиге:
coverageThreshold: {
global: { lines: 80, functions: 80, branches: 75 }
}11. Параметризованные тесты
Одна и та же логика с разными входными данными — типичный кейс. Вместо десятка копипаст-тестов пишем один табличный: набор примеров, которых мы сравниваем через test.each.
test.each([
[2, 3, 5],
[0, 0, 0],
[-1, 1, 0],
])("add(%i, %i) == %i", (a, b, expected) => {
expect(add(a, b)).toBe(expected);
});Плейсхолдеры %i, %s подставляются в имя теста, поэтому в отчёте видно конкретный кейс. Работает и с объектами:
test.each([
{ email: "a@b.ru", valid: true },
{ email: "no-at", valid: false },
{ email: "", valid: false },
])("валидация $email", ({ email, valid }) => {
expect(validateEmail(email)).toBe(valid);
});12. Асинхронные тесты
Работа с API, таймерами и событиями — по умолчанию асинхронная. Jest умеет ждать: возвращаешь промис из теста, и матчеры дождутся результата.
test("асинхронная загрузка", async () => {
const users = await loadUsers();
expect(users).toHaveLength(3);
});А так проверяют, что промис отклоняется с ошибкой:
test("401 без токена", async () => {
await expect(fetchMe()).rejects.toThrow("Unauthorized");
});// классический вариант продолжения
test("данные приходят", () => {
return loadUsers().then((users) => {
expect(users).toHaveLength(3);
});
});return или await: забытый промис — это зелёный тест, который ничего не проверил.13. Jest и TypeScript
Современные проекты пишутся на TypeScript, и Jest с этим дружит. Самый простой путь — ts-jest: та же установка, только тип тестовой среды чуть иной.
npm install --save-dev ts-jest @types/jest// jest.config.ts
export default {
preset: "ts-jest/presets/default-esm",
testEnvironment: "node",
transform: { "^.+\\.tsx?$": "ts-jest" }
};Тесты становятся типизированными:
import { add } from "./math";
describe("add", () => {
it("складывает числа", () => {
expect(add(2, 2)).toBe(4);
});
});14. Отладка и watch-режим
Jest — не только про «запустить один раз и упасть в CI». В разработке он живёт в watch-режиме: перезапускает только тест, который ты меняешь.
# в package.json настроить скрипт
"scripts": {
"test": "jest --watchAll"
}В консоли watch-режима доступны хоткеи: p — фильтр по имени файла, t — по имени теста, o — только изменённые, f — только упавшие.
- запускай
jest -t "имя теста", чтобы не ждать весь набор; - для отладки падающего случая —
--runInBandи breakpoint в IDE; --verboseпокажет каждый тест отдельной строкой.
__snapshots__: если она в репозитории, обновляй снапшоты только осознанно — jest -u после ревью diff.console.log в тесты: мусор в выводе затрудняет чтение отчётов CI, а с --silent его вообще не увидишь.15. Тестирование сетевых запросов: MSW и мок fetch
Тесты не должны ходить в реальный API: их закладывают MSW (Mock Service Worker) или мокают fetch/jest. MSW перехватывает запросы на уровне сети — работает и в браузере, и в Node.
// mocks/handlers.js
import { http, HttpResponse } from 'msw';
export const handler = http.get('/api/users', () =>
HttpResponse.json([{ id: 1, name: 'Ирина' }]));
// setupTests.js
import { setupServer } from 'msw/node';
setupServer(...handlers).listen({ onUnhandledRequest: 'warn' });- проверяйте и удачные, и ошибочные ответы: 500, таймаут, пустой список;
- в обработчиках легко тестировать
loadingиerrorсостояния UI; - альтернатива — jest.mock на модуле fetch: проще, но «глуше» к реальной топологии запроса.
onUnhandledRequest: 'warn' могут тянуть «реальные» запросы в CI и падать на сети. Настраивайте строгий режим.16. Расширение Jest: свои матчеры и expect.extend
Когда проверок не хватает, пишут кастомные матчеры через expect.extend или подключают киты вроде jest-extended.
expect.extend({
toBeWithinRange(received, floor, ceiling) {
const pass = received >= floor && received <= ceiling;
return {
message: () => `ожидалось ${received} в [${floor}, ${ceiling}]`,
pass,
};
},
});
test('цена адекватная', () => {
expect(price).toBeWithinRange(100, 500);
});| Матчеры (jest-extended) | Проверка |
|---|---|
| toBeOneOf, toBeAnyOf | значение из списка |
| toBeEmpty, toBeEmptyArray | пустые коллекции |
| toHaveBeenCalledTimes | число вызовов (вст. в jest) |
setupFilesAfterEnv — они доступны во всех тестах без импорта.expect.extend нельзя переписать существующее поведение частично — возвращайте и pass, и понятное message.17. Скорость тестов: parallel, модули и ресурсы
Большой набор тестов — минуты ожидания. Правильная конфигурация Jest сильно ускоряет прогон без потери надёжности.
// jest.config.js
module.exports = {
maxWorkers: '50%', // параллелизм
moduleNameMapper: { '^@app/(.*)$': '<rootDir>/src/$1' },
testTimeout: 10_000,
slowTestThreshold: 5000, // отметить медленных
};maxWorkers— процессов JEST параллельно; 0.5 от CPU оптимально;- алиасы через
moduleNameMapperускоряют резолюцию путей; - изолируйте тяжёлые модули (базы, файлы) в
jest.isolateModules.
--runInBand для проверки подозрительных случаев.18. Тестирование компонентов вне React: Vue и Angular
Jest работает с любыми компонентами — подключите соответствующий тест-раннер и утилиты для вашего фреймворка.
// Vue 3
import { mount } from '@vue/test-utils';
const wrapper = mount(Counter);
await wrapper.find('button').trigger('click');
expect(wrapper.text()).toContain('1');
// Angular
import { TestBed } from '@angular/core/testing';
const fixture = TestBed.createComponent(AppComponent);Для Angular используют тест-раннер jest-preset-angular, для Vue — vue-jest.
- монтируйте компонент в изоляции с локальными стейтами/моками;
- асинхронные обновления — через
await nextTick(); - переиспользуйте шаблон снапшота (раздел 9), проверяя структуру DOM.
19. Тестирование Node.js-сервисов и CLI-инструментов
Jest отлично работает не только в браузере: серверные модули, CLI-утилиты и фоновые джобы покрываются теми же тестами — с моками файлов, процессов и сети.
const fs = require('fs');
const { run } = require('./cli');
test('CLI выводит результат', () => {
const spy = jest.spyOn(console, 'log')
.mockImplementation();
run(['--file', 'data.json']);
expect(spy).toHaveBeenCalledWith(
expect.stringContaining('OK'));
spy.mockRestore();
});- exec-вызовы и сеть мокаются через jest.mock модулей — без реальных процессов;
- process.env переставляйте в beforeEach/afterEach — тесты изолированы;
- временные файлы создавайте в os.tmpdir() и очищайте после теста.
20. Интеграционные тесты с реальной базой данных
Юнит-тесты с мок-репозиторием не ловят ошибки SQL и транзакций. Интеграционный тест поднимает настоящую БД — SQLite, Postgres в контейнере — и проверяет код целиком.
// jest.config: отдельный набор для интеграционных
testMatch: ['**/integration/**/*.test.js']
// сам тест
const db = await createDb(); // схема + миграции
await insert(db, { id: 1 });
expect(await findBy(db, 1)).toMatchObject({ id: 1 });
await db.destroy();- перед каждым тестом — транзакция с откатом или очистка таблиц;
- параллельные наборы (раздел 17) конфликтуют за БД: либо изолируйте, либо запускайте последовательно;
- миграции применяйте один раз в beforeAll — быстро и воспроизводимо.
21. E2E-тесты: Playwright в связке с Jest
Юнит-тесты подтверждают поведение модулей, но не весь путь пользователя. Playwright управляет реальным браузером: клики, навигация, формы — настоящий сценарий.
// playwright отдельный раннер с тем же стеком
const { test } = require('@playwright/test');
test('пользователь добавляет товар', async ({ page }) => {
await page.goto('/shop');
await page.click('text=Купить');
await expect(page.locator('.cart-count'))
.toHaveText('1');
});- E2E заменяет ручной прогон регрессий и ловит ошибки UI-сборки;
- Playwright имеет собственный раннер — не обязательно заворачивать в Jest;
- баланс в проекте: 70% unit, 20% integration, 10% e2e.
22. Multi-project runner и матрица окружений
Один Jest-конфиг на весь монорепозиторий — боль: серверные и компонентные тесты конфликтуют по окружению и времени. projects запускает несколько конфигов из одного диспетчера.
// jest.config.js
module.exports = {
projects: [
{ displayName: 'unit', testMatch: ['**/unit/**'] },
{ displayName: 'react', preset: 'ts-jest',
testEnvironment: 'jsdom',
testMatch: ['**/components/**'] },
{ displayName: 'integration',
testEnvironment: 'node', testTimeout: 30000,
testMatch: ['**/integration/**'] }
]
};- каждый проект — свой environment, таймаут, пресет и пути;
- запуск нужного набора: jest --selectProjects unit;
- матрица окружений (node 18/20/22) задаётся CI-параметром в одном проекте.
Что дальше
Покрой логику Pinia-сторов и протестируй компоненты React и Vue 3. Типизация тестов — курс TypeScript, код-стайл — ESLint. В новых Vite-проектах рассмотри Vitest (курс Vitest).