В последнее время я все больше уделяю внимание юнит тестированию, что связано с моим наставничеством на Hexlet и выравнивание пирамиды на работе. И немного решил освежить основы при написании юнит тестов:

Быстрота (Fast)

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

Изоляция (Isolated/Independent)

Каждый тест должен быть независим. Он должен следовать модели "подготовка, действие, проверка" (Arrange, Act, Assert) без зависимости от других тестов или внешнего окружения.

Повторяемость (Repeatable)

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

Самодостаточность (Self-Validating)

Результаты теста должны быть ясны без внешних проверок — тест либо проходит, либо нет, без всякой необходимости в дополнительной интерпретации.

Тщательность (Thorough/Timely)

Тесты должны охватывать все возможные сценарии использования и граничные условия, не ограничиваясь простым стремлением к 100% покрытию кода. Важно тестировать различные объемы данных, безопасность, и корректную обработку исключений.


Реальные примеры тестирования в React с использованием принципов FIRST

1. Быстрота (Fast)

// ❌ Плохой тест, который загружает данные из API
it('fetches and displays user data', async () => {
  render(<UserProfile userId="123" />);
  await waitFor(() => expect(screen.getByText(/Username/)).toBeInTheDocument());
});

Этот тест медленный, потому что делает реальный запрос к API.

Хороший пример:

// ✅ Использование моков для ускорения тестов,
// чтобы сделать тест быстрым и независимым
jest.mock('api/userApi');
it('displays user data from mock', () => {
  userApi.getUser.mockResolvedValue({ id: '123', name: 'John Doe' });
  render(<UserProfile userId="123" />);
  expect(screen.getByText(/John Doe/)).toBeInTheDocument();
});

2. Изоляция (Isolated/Independent)

Плохой пример:

// ❌ Эти тесты зависимы друг от друга из-за общего состояния хука
const { result } = renderHook(() => useCounter());
it('increments counter', () => {
  act(() => { result.current.increment(); });
  expect(result.current.count).toBe(1);
});

it('increments counter again', () => {
  act(() => { result.current.increment(); });
  expect(result.current.count).toBe(2);
});

Хороший пример:

// ✅ Каждый тест изолирован
it('increments counter', () => {
  const { result } = renderHook(() => useCounter());
  act(() => { result.current.increment(); });
  expect(result.current.count).toBe(1);
});

it('increments counter independently', () => {
  const { result } = renderHook(() => useCounter());
  act(() => { result.current.increment(); });
  expect(result.current.count).toBe(1);
});

3. Повторяемость (Repeatable)

Плохой пример:

// ❌ Тест зависит от текущей даты
it('shows current date', () => {
  render(<CurrentDateDisplay />);
  const today = new Date().toISOString().slice(0, 10);
  expect(screen.getByText(today)).toBeInTheDocument();
});

Этот тест даст разные результаты каждый день.

Хороший пример:

// ✅ Использование фиксированной даты в тестах,
// что обеспечивает повторяемость результатов
it('shows current date', () => {
  jest.useFakeTimers().setSystemTime(new Date('2024-01-01'));
  render(<CurrentDateDisplay />);
  expect(screen.getByText('2024-01-01')).toBeInTheDocument();
});

4. Самодостаточность (Self-Validating)

Плохой пример:

// ❌ Тест не автоматизирован полностью
it('renders correctly', () => {
  const component = render(<MyComponent />);
  console.log(component); // Требует проверки вывода
});

Хороший пример:

// ✅ Тест с проверкой
it('renders correctly', () => {
  render(<MyComponent />);
  expect(screen.getByText('Hello World')).toBeInTheDocument();
});

Этот тест полностью самодостаточен и не требует внешних действий для проверки.

5. Тщательность (Thorough/Timely)

Плохой пример:

// ❌ Тест проверяет только одно состояние компонента
it('shows loading state', () => {
  render(<DataLoader />);
  expect(screen.getByText('Loading...')).toBeInTheDocument();
});

Тест ограничен только проверкой состояния загрузки.

Хороший пример:

// ✅ Эти тесты охватывают разные возможные состояния компонента
it('shows loading state', () => {
  render(<DataLoader />);
  expect(screen.getByText('Loading...')).toBeInTheDocument();
});

it('displays data after loading', async () => {
  const mockData = { text: 'Data loaded' };
  fetchData.mockResolvedValue(mockData);
  render(<DataLoader />);
  expect(await screen.findByText('Data loaded')).toBeInTheDocument();
});

it('shows error when data fails to load', async () => {
  fetchData.mockRejectedValue('Error loading data');
  render(<DataLoader />);
  expect(await screen.findByText('Error loading data')).toBeInTheDocument();
});

Присоединяйтесь в мой tg, там я пишу различные заметки по QA

Комментарии (3)


  1. icya
    24.07.2024 13:16
    +2

    Сначала мы пишем много тестов.

    Тесты должны охватывать все возможные сценарии использования и граничные условия, не ограничиваясь простым стремлением к 100% покрытию кода.

    Затем замечаем, что их много и начинаем их ускорять

    Тесты должны выполняться очень быстро.

    Пишем моки, чтобы избежать долгих запросов к различным сервисам.

    Проходит время и API этих сервисов меняется, мы запускаем тесты, видим, какое всё зелёненькое и катимся в прод. Прод падает. Правим моки и чиним код.

    У меня когда-то давно была похожая ситуация, код изменился, а моки мокали. Пришлось в срочном порядке чинить всё, что упало. Аккуратнее с моками


    1. hel1n Автор
      24.07.2024 13:16

      Есть разные уровни тестов, в данном контексте мы говорим о юнит тестирование, а есть еще уровень с интеграционными и сквозными тестами которые и должны отловить изменения контракта


    1. Andrey_Solomatin
      24.07.2024 13:16

      Ещё надо не увлечься и не написать тест который тестирует только мок.