1. Зачем нужны unit-тесты?
Unit-тесты создавались для проверки изолированных частей кода — функций, методов, утилит. Их задача — убедиться, что отдельные модули работают корректно в идеальных условиях.
Но фронтенд — это не только логика, но и:
UI-компоненты (кнопки, формы, списки)
API-взаимодействия (запросы, обработка ответов)
Глобальное состояние (Redux, MobX, Context)
Сторонние интеграции (аналитика, платежи)
Можно ли всё это покрыть unit-тестами? Технически — да, но нужно ли?
2. Что не стоит тестировать в unit-тестах?
❌ UI-рендеринг (скриншотные тесты)
Проблема:
Падают при любом изменении вёрстки, даже если логика не сломана.
Требуют постоянного обновления эталонов.
Не ловят реальные баги, только визуальные отличия.
it('renders button', () => {
render(<Button />);
expect(screen.getByRole('button')).toBeInTheDocument();
});
Что делать вместо этого?
Использовать Storybook для ручной проверки компонентов.
Писать интеграционные тесты, проверяя ключевые сценарии.
Для критически важных компонентов (например, платежная форма) — регрессионное визуальное тестирование
❌ API-запросы (с моками)
Проблема:
Моки не отражают реальное поведение API.
Тесты проходят, но на боевом API всё ломается.
Сложность поддержки: при изменении API нужно обновлять моки.
// Хрупкий тест
it('loads user data', async () => {
axios.get.mockResolvedValue({ data: { name: 'John' } });
const { result } = renderHook(() => useUser());
await waitFor(() => {
expect(result.current.user.name).toBe('John');
});
});
Что делать вместо этого?
Использовать Zod для валидации ответов API.
Тестировать реальные запросы в интеграционных/E2E-тестах (Cypress).
❌ Тестирование библиотек и фреймворков
Проблема:
Тестируете не свой код, а чужой (React, Redux, lodash)..
// Бесполезный тест - проверяет работу lodash
it('should add numbers', () => {
expect(add(2, 2)).toBe(4);
});
Что делать?
Доверять тестам самих библиотек.
Если используете кастомные обёртки — тестировать только свою логику.
3. Что нужно тестировать в unit-тестах?
✅ Утилиты и чистые функции
Функции, которые:
Преобразуют данные (
formatDate
,parseQueryString
).Содержат бизнес-логику (
calculateDiscount
,validatePassword
).Легко тестируются без моков и рендеринга.
// utils/validatePassword.js
export function validatePassword(password) {
return password.length >= 8 &&
/[A-Z]/.test(password) &&
/[0-9]/.test(password);
}
// utils/validatePassword.test.js
import { validatePassword } from './validatePassword';
describe('validatePassword', () => {
it('returns true for valid passwords', () => {
expect(validatePassword('ValidPass123')).toBe(true);
});
it('returns false for invalid passwords', () => {
expect(validatePassword('short')).toBe(false);
expect(validatePassword('nouppercase123')).toBe(false);
expect(validatePassword('NoNumbersHere')).toBe(false);
});
});
✅ Кастомные хуки
Хуки с логикой (формами, состоянием, API-вызовами) — отличные кандидаты для unit-тестов.
// hooks/useCounter.js
import { useState } from 'react';
export function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(c => c + 1);
const decrement = () => setCount(c => c - 1);
const reset = () => setCount(initialValue);
return { count, increment, decrement, reset };
}
// hooks/useCounter.test.js
import { renderHook, act } from '@testing-library/react-hooks';
import { useCounter } from './useCounter';
describe('useCounter', () => {
it('should increment counter', () => {
const { result } = renderHook(() => useCounter());
act(() => result.current.increment());
expect(result.current.count).toBe(1);
});
it('should reset counter', () => {
const { result } = renderHook(() => useCounter(5));
act(() => result.current.increment());
act(() => result.current.reset());
expect(result.current.count).toBe(5);
});
});
✅ Сложная бизнес-логика
Если в компоненте есть нетривиальные вычисления — их стоит вынести в отдельную функцию и протестировать.
// cartUtils.js
export function calculateTotal(items, discount = 0) {
const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
const discountAmount = subtotal * (discount / 100);
return subtotal - discountAmount;
}
// cartUtils.test.js
import { calculateTotal } from './cartUtils';
describe('calculateTotal', () => {
it('calculates total without discount', () => {
const items = [
{ price: 10, quantity: 2 },
{ price: 5, quantity: 3 }
];
expect(calculateTotal(items)).toBe(35);
});
it('applies discount correctly', () => {
const items = [
{ price: 20, quantity: 1 },
{ price: 10, quantity: 2 }
];
expect(calculateTotal(items, 10)).toBe(36); // 40 - 10%
});
it('handles empty cart', () => {
expect(calculateTotal([])).toBe(0);
});
});
4. Альтернативы unit-тестам
Логика, утилиты Unit-тесты (Jest, Vitest)
API-взаимодействия Контрактные тесты (Zod)
Интеграция компонентов Интеграционные тесты
5. От боли к качеству: эволюция тестирования в большом проекте.
Проект был большим и давно живущим. Изначально никакого тестирования не существовало вовсе, а первые попытки внедрения были хаотичными и неудачными. Каждый новый тест становился испытанием — приходилось разбираться с тонкостями инфраструктуры, искать баланс между простотой реализации и покрытием функциональности. Основная сложность заключалась в неправильном подходе, мы смотрели не на надежность системы, а на процент покрытия. Первые тесты, направленные на проверку отображения интерфейсов, так как легко повысить процент покрытия, но это быстро превратилось в головную боль. Простое сравнение скриншотов оказалось ненадежным решением — любые изменения стилей приводили к ошибкам теста, хотя функциональность оставалась рабочей, а изменения были запланированными или ожидаемыми и входили в норму. Мы поняли, что такие тесты неэффективны и лучше сосредоточиться на проверке поведения элементов, а не внешнего вида. Постепенно отказались от скриншотных тестов, положившись на работу тестировщика и его автоматизированных тестов.
Работа с API. Следующая проблема возникла с покрытием API-запросов. Использование моковых данных привело к появлению хрупких тестов — мы постоянно сталкивались с необходимостью переопределять моки при каждом незначительном изменении контракта, так как тесты мы писали в преддверье большого изменения апи, это было недопустимо. Чтобы справиться с этими проблемами, мы начали постепенно внедрять Zod и ввели регулярную ручную проверку ключевых страниц перед каждым релизом. В итоге процесс пошел быстрее и стабильнее.
Ну и личная головная боль, покрытие тестами целых компонентов — самое бесполезное занятие, которое можно представить, так как либо это повторное тестирование логики, так как условный рендеринг строится на результате работы утилит, либо проверка API, которое уже проверил Zod.
Сейчас проект покрывает всего на 20-30 процентов unit тестами, но при этом Zod полностью закрывает проблему с API, unit тесты проверяют бизнес-логику и утилиты, а визуальное поведение проверяется за счет интеграционных тестов и ручной проверки перед релизом, которая занимает меньше часа.
Вывод
Unit-тесты нужны для логики, утилит и хуков.
Не нужны для UI-рендеринга, API (с моками) и библиотек.
Лучше меньше, но качественнее — тесты должны ловить баги, а не просто быть.
Дополняйте unit-тесты интеграционными и E2E-проверками.
Тесты должны экономить время, а не создавать лишнюю работу. ?
Полезные ссылки:
Комментарии (2)
Madrusnl
19.08.2025 09:40Разделяю совет не увлекаться юнит тестами чисто ради 100% покрытия. Zod сильно помогает, также как Playwright или Cypress. Я обычно подключаю для создания тестов AI, но всегда смотрю, что именно они тестируют. Удалить лишнее - не долго. Один момент можно было бы усилить: для примеров использовать последние версии библиотек. Так, например, `@testing-library/react-hooks` уже уступила место `@testing-library/react`.
Vitaly_js
Не понял всей этой темы с отрицанием необходимости тестировать АПИ, кроме разумеется того как вы это делаете. Так делать, конечно, не стоит.
Если у вас есть юнит-тест, который учитывает схему данных полученных по сети, то вы тестируете всю цепочку от получения данных до отображения на экране. Меняется апи и вам нужно поменять схему зода, если надо модельки, но при этом в самом тесте меняется только мок и только то, что должно поменяться на экране.
Почему не нужно делать вот так:
Если у вас есть сторибук и не один юнит тест, то должна быть отдельная функциональность для генерации данных для моков. Если такая функциональность есть, то вносить изменения нужно в одно место, а не во все юнит тесты. И тогда ваши юнит тесты будут тестировать и схемы zod, и другие изменения данных, которые не влияют на предыдущую функциональность.
И, разумеется, если у вас настроен msw для тестов, то добавлять `axios.get.mockResolvedValue({ data: { name: 'John' } });` постоянно не обязательно.