Вы запускаете npm test, а 15 тестов падают из-за нестабильных моков или неправильной конфигурации. Знакомая ситуация? Мы помогаем наладить надёжное unit-тестирование: настраиваем Jest, подключаем правильные утилиты и покрываем критичные модули тестами. В результате ваша команда перестаёт бояться рефакторинга, а CI выдаёт стабильный зелёный статус. Наша команда имеет 7+ лет опыта в автоматизации тестирования и выполнила 50+ проектов по unit-тестам для фронтенда. Мы работаем с проектами любой сложности: от одностраничных приложений до крупных корпоративных порталов. Мы не просто пишем тесты — мы проектируем тестируемую архитектуру.
По статистике наших проектов, внедрение unit-тестов сокращает время регрессионного тестирования на 40% и уменьшает количество багов в продакшене на 25%. Это прямая экономия бюджета на QA и поддержку — в среднем 2-3 месяца работы QA, что составляет экономию от 200 000 до 500 000 рублей в год на команду из 5 разработчиков. Более того, хорошо написанные тесты служат документацией и упрощают онбординг разработчиков.
Однако многие команды сталкиваются с типичными трудностями: нестабильные тесты из-за неверных моков, отсутствие покрытия кастомных хуков и сложная конфигурация Jest с TypeScript. Разберёмся, как решить эти проблемы, на примере наших проектов.
Какие проблемы решает unit-тестирование?
- Нестабильные тесты из-за неверных mock'ов и грязного окружения. Типичный проект с 20-30 компонентами требует аккуратного управления состоянием и моками.
- Отсутствие покрытия для кастомных хуков и редьюсеров — логика остаётся без защиты. Без тестов каждый рефакторинг — риск.
- Сложная конфигурация Jest+React+TypeScript — путаница с трансформацией, алиасами и стилями. Мы настраиваем всё от
jest.config.tsдоsetupFilesAfterFramework, используя современный стек: Jest 29,@swc/jestдля быстрой трансформации, React Testing Library для компонентов, MSW для моков API.
Настройка занимает 2-4 часа, а результат — стабильный CI и ощутимая экономия бюджета.
Как настроить Jest для React-проекта с TypeScript?
Стандартная конфигурация включает:
npm install -D jest @types/jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom
// jest.config.ts
export default {
testEnvironment: 'jsdom',
setupFilesAfterFramework: ['<rootDir>/jest.setup.ts'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'\.(css|scss)$': 'identity-obj-proxy',
},
transform: {
'^.+\.(ts|tsx)$': ['@swc/jest'],
},
coverageThreshold: {
global: { branches: 70, functions: 80, lines: 80 },
},
};
Эта конфигурация поддерживает алиасы, CSS-модули и TypeScript. coverageThreshold гарантирует минимальный уровень покрытия — иначе пайплайн упадёт. Подробнее о настройке можно прочитать в официальной документации Jest.
Для проектов на Next.js или Vue потребуются небольшие изменения: добавьте testEnvironment: 'jsdom' и настройте трансформацию под ваш фреймворк. Мы подбираем конфиг индивидуально.
Почему стоит тестировать кастомные хуки и асинхронные запросы?
Кастомные хуки содержат бизнес-логику, которая повторяется в разных компонентах. Без тестов вы рискуете получить неожиданное поведение при изменении стейта. API-запросы — узкое место: меняется бэкенд, падает таймаут, возвращается 404. MSW перехватывает запросы на уровне сети, что быстрее и надёжнее ручных mock'ов.
Сравнение подходов к мокированию API
| Инструмент | Тип мока | Производительность | Реалистичность |
|---|---|---|---|
| MSW | Service Worker / Node.js | Высокая (нативный перехват) | Полная (эмуляция сети) |
| jest.mock | Подмена модуля | Средняя | Частичная (нет HTTP-статусов) |
| nock | HTTP-перехват | Высокая | Полная (но вне браузера) |
MSW лучше ручных mock'ов в 2–3 раза по скорости выполнения тестов и точнее имитирует поведение сервера.
Что входит в работу
- Настроенный Jest с конфигом под ваш стек.
- 30–50 unit-тестов на утилиты, хуки, компоненты и сервисы.
- Интеграция с CI/CD (GitHub Actions, GitLab CI, Jenkins).
- Документация по запуску, добавлению новых тестов и работе с моками.
- Гарантия стабильности: тесты проходят, coverage не ниже оговорённого порога (обычно 80%).
Примеры тестов
Тестирование утилит
// src/utils/currency.ts
export const formatCurrency = (amount: number, locale = 'ru-RU', currency = 'RUB') =>
new Intl.NumberFormat(locale, { style: 'currency', currency }).format(amount);
// src/utils/currency.test.ts
describe('formatCurrency', () => {
it('formats RUB correctly', () => {
expect(formatCurrency(1500)).toMatch('1 500');
});
it('handles zero', () => {
expect(formatCurrency(0)).toMatch('0');
});
it('formats USD', () => {
expect(formatCurrency(99.99, 'en-US', 'USD')).toBe('$99.99');
});
});
Тестирование React-компонентов
// components/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from './Button';
describe('Button', () => {
it('renders label', () => {
render(<Button>Сохранить</Button>);
expect(screen.getByRole('button', { name: 'Сохранить' })).toBeInTheDocument();
});
it('calls onClick', async () => {
const onClick = jest.fn();
render(<Button onClick={onClick}>Click me</Button>);
await userEvent.click(screen.getByRole('button'));
expect(onClick).toHaveBeenCalledTimes(1);
});
it('disabled button does not fire onClick', async () => {
const onClick = jest.fn();
render(<Button onClick={onClick} disabled>Disabled</Button>);
await userEvent.click(screen.getByRole('button'));
expect(onClick).not.toHaveBeenCalled();
});
it('shows loading spinner when loading', () => {
render(<Button loading>Save</Button>);
expect(screen.getByRole('button')).toHaveAttribute('aria-busy', 'true');
expect(screen.getByTestId('spinner')).toBeInTheDocument();
});
});
Тестирование API-запросов
// services/api.test.ts
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { fetchUser } from './api';
const server = setupServer(
rest.get('/api/users/:id', (req, res, ctx) => {
return res(ctx.json({ id: 1, name: 'Иван' }));
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test('fetchUser returns user data', async () => {
const user = await fetchUser(1);
expect(user.name).toBe('Иван');
});
test('fetchUser handles 404', async () => {
server.use(
rest.get('/api/users/:id', (req, res, ctx) => res(ctx.status(404)))
);
await expect(fetchUser(999)).rejects.toThrow('Not found');
});
Процесс работы
- Анализ проекта: структура, основные модули, критические пути. Занимает 2–4 часа.
- Настройка Jest и тестового окружения. Подключаем библиотеки, прописываем алиасы.
- Написание тестов (2–4 дня) с приоритетом на бизнес-логику. Пишем тесты на утилиты, хуки, компоненты и API.
- Интеграция в CI — добавление шага
npm test -- --coverage. - Передача документации и консультация команды.
Сроки выполнения
| Объём работ | Срок |
|---|---|
| Базовый (настройка + 30–40 тестов) | 3–5 дней |
| Расширенный (дополнительно тесты на сложные сценарии) | 6–10 дней |
| Полное покрытие legacy-проекта | от 2 недель |
Точные сроки называем после аудита вашего проекта. Оставьте заявку — мы оценим объём и предложим варианты.
Свяжитесь с нами для консультации — расскажем, как улучшить покрытие и ускорить CI. Закажите unit-тесты под ключ и получите стабильный продакшен.







