Тестирование с React Testing Library

Тестирование React-компонентов, использующих TanStack Query, требует учета асинхронной природы данных, кэширования, повторных запросов и поведения QueryClient. В связке с React Testing Library основной задачей становится контроль сетевого слоя, управление жизненным циклом QueryClient и детерминированная имитация серверных ответов.


Базовая проблема при тестировании компонентов с TanStack Query заключается в том, что QueryClient живет вне React-компонента и управляет кэшем глобально. Это приводит к утечкам состояния между тестами, если не изолировать экземпляры.

Ключевой подход — создание фабрики QueryClient для каждого теста.

import { QueryClient } from '@tanstack/react-query';

export function createTestQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        retry: false,
        gcTime: 0,
        staleTime: 0,
      },
    },
  });
}

Отключение retry критично: иначе тесты могут флапать из-за повторных попыток запроса.


Обертка провайдера для тестов

Компоненты с использованием useQuery требуют QueryClientProvider. В тестовой среде удобнее создать кастомный render.

import React from 'react';
import { QueryClientProvider } from '@tanstack/react-query';
import { render } from '@testing-library/react';
import { createTestQueryClient } from './createTestQueryClient';

export function renderWithQuery(ui) {
  const queryClient = createTestQueryClient();

  function Wrapper({ children }) {
    return (
      <QueryClientProvider client={queryClient}>
        {children}
      </QueryClientProvider>
    );
  }

  return {
    queryClient,
    ...render(ui, { wrapper: Wrapper }),
  };
}

Такой подход позволяет в тестах напрямую управлять кэшем через returned queryClient.


Тестирование загрузочного состояния

Компоненты с useQuery почти всегда имеют состояние loading. React Testing Library хорошо работает с этим через findBy queries.

import { screen } from '@testing-library/react';
import { renderWithQuery } from './renderWithQuery';
import { rest } from 'msw';
import { setupServer } from 'msw/node';

const server = setupServer(
  rest.get('/api/users', (req, res, ctx) => {
    return res(ctx.json([{ id: 1, name: 'Alex' }]));
  })
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test('renders users after loading', async () => {
  renderWithQuery(<Users />);

  expect(screen.getByText(/loading/i)).toBeInTheDocument();

  const user = await screen.findByText('Alex');
  expect(user).toBeInTheDocument();
});

Использование MSW позволяет тестировать слой запросов без моков fetch или axios.


Контроль запроса через MSW

Стабильность тестов обеспечивается тем, что сетевые ответы контролируются через Mock Service Worker.

Можно тестировать как успешные, так и ошибочные сценарии.

server.use(
  rest.get('/api/users', (req, res, ctx) => {
    return res(ctx.status(500));
  })
);
test('renders error state', async () => {
  renderWithQuery(<Users />);

  const error = await screen.findByText(/error/i);
  expect(error).toBeInTheDocument();
});

Работа с кэшем в тестах

Одно из ключевых преимуществ TanStack Query — кэш. В тестах его поведение часто мешает, если не управлять им явно.

QueryClient позволяет напрямую модифицировать состояние:

test('uses cached data', async () => {
  const { queryClient, rerender } = renderWithQuery(<Users />);

  queryClient.setQueryData(['users'], [
    { id: 1, name: 'Cached User' },
  ]);

  rerender(<Users />);

  const user = await screen.findByText('Cached User');
  expect(user).toBeInTheDocument();
});

Такой подход полезен для проверки UI без повторного запроса.


Инвалидация запросов в тестах

Mutation часто вызывает invalidateQueries, и это поведение важно проверять.

import { useMutation, useQueryClient } from '@tanstack/react-query';

function useCreateUser() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (user) =>
      fetch('/api/users', {
        method: 'POST',
        body: JSON.stringify(user),
      }),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] });
    },
  });
}

Тестирование:

test('invalidates users query after mutation', async () => {
  const { queryClient } = renderWithQuery(<Users />);

  queryClient.setQueryData(['users'], [{ id: 1, name: 'Old' }]);

  // имитация invalidation
  queryClient.invalidateQueries({ queryKey: ['users'] });

  expect(queryClient.isFetching()).toBe(true);
});

Проверка мутаций через пользовательские действия

React Testing Library ориентирована на поведение пользователя, поэтому мутации тестируются через события.

test('creates new user', async () => {
  renderWithQuery(<CreateUserForm />);

  userEvent.type(screen.getByPlaceholderText('Name'), 'John');
  userEvent.click(screen.getByText('Create'));

  const user = await screen.findByText('John');
  expect(user).toBeInTheDocument();
});

При этом важно контролировать backend через MSW, иначе тест становится нестабильным.


Отключение повторных запросов и флапающих тестов

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

Настройки:

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: false,
      refetchOnWindowFocus: false,
    },
  },
});

Также важно отключать refetchOnMount, если тест проверяет только UI-состояние.


Тестирование состояний refetch и stale

Поведение stale-данных часто становится источником скрытых багов.

queryClient.setQueryData(['users'], data);
queryClient.invalidateQueries(['users']);

После этого useQuery должен перейти в состояние fetching, сохранив предыдущие данные. Это можно проверять через UI:

expect(screen.getByText('Old Data')).toBeInTheDocument();
expect(await screen.findByText('Updated Data')).toBeInTheDocument();

Асинхронные гонки и waitFor

React Testing Library предоставляет waitFor для синхронизации с TanStack Query.

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

await waitFor(() => {
  expect(screen.getByText('Loaded')).toBeInTheDocument();
});

waitFor особенно важен при сложных зависимостях queries, когда один запрос зависит от результата другого.


Изоляция QueryClient между тестами

Общий QueryClient между тестами приводит к утечке состояния кэша.

Правильная стратегия — новый клиент на каждый render:

  • уникальный QueryClient
  • сброс кэша после каждого теста
  • отключенный gcTime

Это устраняет эффект “памяти” между тестами.


Тестирование dependent queries

Когда один query зависит от другого:

useQuery({
  queryKey: ['user', id],
  queryFn: () => fetchUser(id),
  enabled: !!id,
});

Тестирование должно учитывать состояние enabled:

test('does not fetch without id', () => {
  renderWithQuery(<User userId={null} />);

  expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
});

Проверка состояния isFetching и isLoading

TanStack Query разделяет:

  • isLoading — первый запрос
  • isFetching — любой активный запрос

В тестах важно различать эти состояния.

expect(queryClient.isFetching()).toBe(0);

или через UI:

expect(screen.getByText(/loading/i)).toBeInTheDocument();

Комбинация MSW и QueryClient для детерминизма

Стабильная стратегия тестирования строится на трех слоях:

  • MSW контролирует HTTP
  • QueryClient контролирует кэш и поведение запросов
  • React Testing Library проверяет UI

Эта комбинация устраняет необходимость мокать fetch вручную и снижает связанность тестов с реализацией.


Частые ошибки в тестах с TanStack Query

  • использование одного QueryClient на все тесты
  • отсутствие retry:false
  • отсутствие cleanup MSW
  • попытка тестировать внутренние состояния вместо UI
  • игнорирование кэша при проверке мутаций
  • использование setTimeout вместо waitFor

Эти ошибки приводят к нестабильным тестам и ложным падениям.


Управление временем и refetch интервалами

Если включены refetchInterval или background updates, тесты могут становиться недетерминированными.

Решение — отключение интервалов в тестовой конфигурации:

refetchInterval: false

или полное управление таймерами через fake timers при необходимости.


Проверка optimistic updates

Optimistic updates требуют проверки промежуточного состояния UI.

onMutate: async (newUser) => {
  queryClient.setQueryData(['users'], (old) => [...old, newUser]);
}

Тест:

userEvent.click(screen.getByText('Create'));

expect(screen.getByText('Temp User')).toBeInTheDocument();

Тестирование с React Testing Library и TanStack Query строится вокруг управления состоянием QueryClient, контроля сети через MSW и проверки пользовательского поведения вместо внутренних механизмов библиотеки.