beforeEach, afterEach для setup и cleanup

В модульном и интеграционном тестировании React-приложений важна строгая изоляция тестов. Каждый тест должен выполняться в предсказуемом состоянии, не зависящем от предыдущих запусков. Для этого в экосистеме JavaScript-тестирования используются хуки beforeEach и afterEach, предоставляемые тест-раннерами (чаще всего Jest или Vitest). В контексте React Testing Library эти хуки применяются для setup и cleanup тестового окружения.


Роль beforeEach в setup

beforeEach выполняется перед каждым тестом (test / it) внутри одного describe-блока. Его задача — подготовить окружение в единообразном состоянии.

Типовые сценарии использования:

  • рендер React-компонента;
  • настройка mock-функций;
  • инициализация тестовых данных;
  • установка глобальных конфигураций (например, локали или feature-флагов);
  • настройка fake timers.

Базовый пример

import { render, screen } from '@testing-library/react';
import UserProfile from './UserProfile';

describe('UserProfile', () => {
  beforeEach(() => {
    render(<UserProfile />);
  });

  test('отображает имя пользователя', () => {
    expect(screen.getByText('Иван')).toBeInTheDocument();
  });

  test('отображает аватар', () => {
    expect(screen.getByRole('img')).toBeInTheDocument();
  });
});

Рендер выполняется один раз на каждый тест, что гарантирует чистое DOM-состояние.


Setup с параметрами и зависимостями

Часто компонент зависит от пропсов, контекста или провайдеров. В этом случае beforeEach удобно использовать вместе с фабрикой или конфигурационным объектом.

let defaultProps;

beforeEach(() => {
  defaultProps = {
    isAdmin: false,
    userName: 'Иван',
  };
});

Или с кастомным рендером:

const renderWithProviders = (ui) => {
  return render(
    <AuthProvider>
      <ThemeProvider>{ui}</ThemeProvider>
    </AuthProvider>
  );
};

beforeEach(() => {
  renderWithProviders(<Dashboard />);
});

Такой подход:

  • уменьшает дублирование;
  • делает тесты короче;
  • упрощает поддержку при изменении инфраструктуры.

afterEach и cleanup тестового окружения

afterEach выполняется после каждого теста и предназначен для очистки побочных эффектов.

В React Testing Library очистка DOM выполняется автоматически через cleanup, если используется стандартная конфигурация Jest. Однако afterEach остаётся критически важным в ряде случаев:

  • сброс mock-функций;
  • восстановление таймеров;
  • очистка глобального состояния;
  • удаление подписок и слушателей.

Пример с mock-функциями

afterEach(() => {
  jest.clearAllMocks();
});

Это гарантирует, что вызовы mock-функций не «протекут» в следующий тест.


Явный cleanup и контроль жизненного цикла

Иногда автоматическая очистка недостаточна или отключена (например, в нестандартной конфигурации).

import { cleanup } from '@testing-library/react';

afterEach(() => {
  cleanup();
});

Явный вызов полезен при:

  • тестировании legacy-кода;
  • использовании нескольких DOM-контейнеров;
  • сложных сценариях с порталом (createPortal).

Асинхронный setup и teardown

beforeEach и afterEach поддерживают async/await, что критично для тестирования эффектов, запросов и инициализации состояния.

beforeEach(async () => {
  await fetchMock.enableMocks();
  fetchMock.mockResponseOnce(JSON.stringify({ name: 'Иван' }));
  render(<UserProfile />);
});
afterEach(async () => {
  await fetchMock.disableMocks();
});

Асинхронный teardown особенно важен при:

  • работе с WebSocket;
  • использовании fake timers;
  • управлении глобальными ресурсами.

Работа с таймерами

При использовании jest.useFakeTimers() состояние таймеров должно быть строго контролируемым.

beforeEach(() => {
  jest.useFakeTimers();
});

afterEach(() => {
  jest.runOnlyPendingTimers();
  jest.useRealTimers();
});

Такой шаблон предотвращает зависание тестов и утечки таймеров между кейсами.


Многоуровневые describe и порядок выполнения

Хуки выполняются иерархически:

  • beforeEach внешнего describe → внутреннего;
  • afterEach — в обратном порядке.
describe('Auth', () => {
  beforeEach(() => {
    // общий setup
  });

  describe('Logged in user', () => {
    beforeEach(() => {
      // дополнительный setup
    });

    test('...', () => {});
  });
});

Это позволяет:

  • разделять общую и специфичную инициализацию;
  • избегать сложных условий внутри одного beforeEach.

Антипаттерны при использовании beforeEach и afterEach

Избыточная логика

Сложные вычисления, условия и ветвления в хуках снижают читаемость тестов.

Скрытые зависимости

Тесты, которые неявно зависят от beforeEach, труднее читать и поддерживать. Критически важные данные должны быть видны в самом тесте или в явно названных фабриках.

Мутация общих объектов

let user;

beforeEach(() => {
  user = { name: 'Иван' };
});

Допустимо, но только при гарантии, что объект не мутируется в тестах. В противном случае — глубокое клонирование или фабрика.


Практика: баланс между хуками и явным кодом

Хуки должны:

  • подготавливать окружение;
  • устранять повторяющийся boilerplate;
  • не скрывать бизнес-логику теста.

Оптимальный стиль — минимальный beforeEach + явная логика в теле теста.

beforeEach(() => {
  render(<LoginForm />);
});

test('отправляет форму', async () => {
  userEvent.type(screen.getByLabelText(/email/i), 'test@mail.com');
  userEvent.click(screen.getByRole('button'));
});

Такой подход сохраняет прозрачность и делает тесты самодокументируемыми.


Связь с философией React Testing Library

React Testing Library продвигает принципы:

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

beforeEach и afterEach служат вспомогательными инструментами, обеспечивающими стабильную среду, но не должны становиться центром тестовой логики.