You run npm test and 15 tests fail due to unstable mocks or incorrect configuration. Sound familiar? We help set up reliable unit testing: configure Jest, connect the right utilities, and cover critical modules with tests. As a result, your team stops fearing refactoring, and CI produces a steady green status. Our team has 7+ years of test automation experience and has completed 50+ unit test projects for frontend. We have been in the frontend testing market for 5+ years. We work with projects of any complexity: from single-page apps to large corporate portals. We don't just write tests — we design testable architecture.
According to our project statistics, implementing unit tests reduces regression testing time by 40% and decreases bugs in production by 25%. That's a direct savings on QA and support budget — on average 2–3 months of QA work, which amounts to savings of 200,000 to 500,000 rubles per year for a team of 5 developers. Moreover, well-written tests serve as documentation and simplify developer onboarding.
However, many teams face typical challenges: flaky tests due to incorrect mocks, lack of coverage for custom hooks, and complex Jest configuration with TypeScript. Let's break down how to solve these problems using examples from our projects.
What problems does unit testing solve?
- Flaky tests due to incorrect mocks and dirty environment. A typical project with 20–30 components requires careful state management and mocking.
- Lack of coverage for custom hooks and reducers — logic remains unprotected. Without tests, every refactoring is a risk.
- Complex Jest+React+TypeScript configuration — confusion with transforms, aliases, and styles. We configure everything from
jest.config.tstosetupFilesAfterFramework, using a modern stack: Jest 29,@swc/jestfor fast transforms, React Testing Library for components, MSW for API mocking.
Setup takes 2–4 hours, and the result is a stable CI and tangible budget savings.
How to configure Jest for a React project with TypeScript?
Standard configuration includes:
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 },
},
};
This configuration supports aliases, CSS modules, and TypeScript. coverageThreshold guarantees a minimum coverage level — otherwise the pipeline fails. For more details on setup, refer to official Jest documentation.
For projects on Next.js or Vue, minor changes are needed: add testEnvironment: 'jsdom' and configure transforms for your framework. We tailor the config individually.
Why test custom hooks and async requests?
Custom hooks contain business logic that repeats across components. Without tests, you risk unexpected behavior when state changes. API requests are a bottleneck: backend changes, timeout failures, 404 returns. MSW intercepts requests at the network level, which is faster and more reliable than manual mocks.
Comparison of API mocking approaches
| Tool | Mock Type | Performance | Realism |
|---|---|---|---|
| MSW | Service Worker / Node.js | High (native interception) | Full (network emulation) |
| jest.mock | Module substitution | Medium | Partial (no HTTP statuses) |
| nock | HTTP interception | High | Full (but outside browser) |
MSW outperforms manual mocks by 2–3 times in test execution speed and more accurately simulates server behavior.
What's included in the work
- Configured Jest with config tailored to your stack.
- 30–50 unit tests for utilities, hooks, components, and services.
- CI/CD integration (GitHub Actions, GitLab CI, Jenkins).
- Documentation on running, adding new tests, and working with mocks.
- Stability guarantee: tests pass, coverage does not fall below agreed threshold (usually 80%).
Example tests
Testing utilities
// 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');
});
});
Testing React components
// 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>Save</Button>);
expect(screen.getByRole('button', { name: 'Save' })).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();
});
});
Testing API requests
// 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: 'John' }));
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test('fetchUser returns user data', async () => {
const user = await fetchUser(1);
expect(user.name).toBe('John');
});
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');
});
Additional test scenarios (click to expand)
We also cover edge cases like network errors, timeout, and empty responses using MSW. For each API endpoint, we write tests for success, 4xx, 5xx, and network failure. This ensures robust error handling.
Our approach: a real case study
On a recent e-commerce project, we inherited a React codebase with 150+ components and zero tests. The CI pipeline took 45 minutes, and every deploy was risky. We configured Jest with @swc/jest and MSW, then wrote 80 tests covering core business logic (cart, checkout, product listing). Test execution time dropped from 12 minutes to 3 minutes. The coverage threshold of 80% was met, and the regression bug rate decreased by 60%. The client saved approximately 2 months of QA effort per release cycle.
Process of work
- Project analysis: structure, main modules, critical paths. Takes 2–4 hours.
- Jest and test environment setup. Connect libraries, define aliases.
- Writing tests (2–4 days) prioritizing business logic. Write tests for utilities, hooks, components, and API.
- CI integration — add
npm test -- --coveragestep. - Hand over documentation and consult the team.
Timeline estimates
| Scope of work | Timeframe |
|---|---|
| Basic (setup + 30–40 tests) | 3–5 days |
| Extended (additional complex scenario tests) | 6–10 days |
| Full coverage of legacy project | from 2 weeks |
We provide exact timelines after auditing your project. Contact us for a consultation — we'll assess the scope and suggest options.
Reach out to us for a consultation — we'll tell you how to improve coverage and speed up CI. Order turnkey unit tests and get a stable production.







