TanStack Query: Майстерність Керування Станом Сервера

Стратегії Тестування

Тестування асинхронного коду — це біль. "Flaky tests" (тести, що падають через раз), тайм-аути, проблеми з API...

Стратегії Тестування

Тестування асинхронного коду — це біль. "Flaky tests" (тести, що падають через раз), тайм-аути, проблеми з API...

TanStack Query робить тестування набагато простішим, але потребує правильного налаштування.

Золотий Стандарт: MSW (Mock Service Worker)

Не намагайтеся мокати сам useQuery або axios. Це погана практика, бо ви тестуєте деталі реалізації. Тестуйте поведінку: "Коли користувач заходить на сторінку, він бачить дані".

Використовуйте MSW, щоб перехоплювати мережеві запити на рівні Node.js (у тестах).

Налаштування Test Wrapper

У кожному тесті вам потрібен QueryClientProvider. Але не використовуйте той самий клієнт, що й в App.tsx.

  1. Retry: Вимкніть retry. В тестах ви не хочете чекати 3 рази по 5 секунд, поки тест впаде.
  2. Cache: Очищайте кеш перед кожним тестом (а краще створюйте новий клієнт).
test-utils.tsx
import { render } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const createTestQueryClient = () => new QueryClient({
  defaultOptions: {
    queries: {
      retry: false, // 🛑 Не повторювати запити
      gcTime: Infinity,
    },
  },
});

export function renderWithClient(ui: React.ReactElement) {
  const testQueryClient = createTestQueryClient();
  const { rerender, ...result } = render(
    <QueryClientProvider client={testQueryClient}>{ui}</QueryClientProvider>
  );
  
  return {
    ...result,
    rerender: (rerenderUi: React.ReactElement) =>
      rerender(
        <QueryClientProvider client={testQueryClient}>{rerenderUi}</QueryClientProvider>
      ),
  };
}

Тестування Компонентів

Припустимо, у нас є компонент UserList.

UserList.test.tsx
import { screen } from '@testing-library/react';
import { renderWithClient } from './test-utils';
import { UserList } from './UserList';
import { server } from './mocks/server';
import { http, HttpResponse } from 'msw';

test('показує скелетон при завантаженні', () => {
  // Налаштовуємо мок так, щоб він "висів" (delay: infinite)
  server.use(
    http.get('/api/users', async () => {
      await delay('infinite');
      return HttpResponse.json([]);
    })
  );

  renderWithClient(<UserList />);
  expect(screen.getByText(/loading/i)).toBeInTheDocument();
});

test('показує користувачів після завантаження', async () => {
  // Стандартний успішний мок
  server.use(
    http.get('/api/users', () => {
      return HttpResponse.json([{ id: 1, name: 'John Doe' }]);
    })
  );

  renderWithClient(<UserList />);

  // waitFor чекає, поки елемент з'явиться (асинхронно)
  expect(await screen.findByText('John Doe')).toBeInTheDocument();
});

test('показує помилку, якщо сервер впав', async () => {
  server.use(
    http.get('/api/users', () => {
      return new HttpResponse(null, { status: 500 });
    })
  );

  renderWithClient(<UserList />);

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

Тестування Кастомних Хуків (renderHook)

Якщо у вас є складна логіка в хуку (наприклад, селектори або side-effects), тестуйте хук ізольовано.

useUser.test.tsx
import { renderHook, waitFor } from '@testing-library/react';
import { createWrapper } from './test-utils'; // Wrapper з Provider
import { useUser } from './useUser';

test('повертає дані користувача', async () => {
  const { result } = renderHook(() => useUser(), {
    wrapper: createWrapper(),
  });

  // 1. Спочатку loading
  expect(result.current.isPending).toBe(true);

  // 2. Чекаємо успіху
  await waitFor(() => expect(result.current.isSuccess).toBe(true));

  // 3. Перевіряємо дані
  expect(result.current.data).toEqual({ name: 'John Doe' });
});

"I wait for something but it never happens"

Типова помилка: Jest/Vitest завершує тест, але QueryClient все ще має активні таймери або проміси. Ви побачите помилку: Jest did not exit one second after the test run has completed.

Рішення: переконайтеся, що ви очищаєте клієнт або використовуєте await для всіх асинхронних дій.

Якщо ви використовуєте Vitest, він чудово працює з TanStack Query "з коробки". Жодних додаткових налаштувань для таймерів зазвичай не потрібно.
Copyright © 2026