Search
Write a publication
Pull to refresh

Unit тесты в React разработке

Level of difficultyEasy
Reading time5 min
Views1K

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-проверками.

Тесты должны экономить время, а не создавать лишнюю работу. 🚀

Полезные ссылки:

Tags:
Hubs:
+3
Comments2

Articles