Стратегії Тестування
Стратегії Тестування
Тестування асинхронного коду — це біль. "Flaky tests" (тести, що падають через раз), тайм-аути, проблеми з API...
TanStack Query робить тестування набагато простішим, але потребує правильного налаштування.
Золотий Стандарт: MSW (Mock Service Worker)
Не намагайтеся мокати сам useQuery або axios. Це погана практика, бо ви тестуєте деталі реалізації.
Тестуйте поведінку: "Коли користувач заходить на сторінку, він бачить дані".
Використовуйте MSW, щоб перехоплювати мережеві запити на рівні Node.js (у тестах).
Налаштування Test Wrapper
У кожному тесті вам потрібен QueryClientProvider. Але не використовуйте той самий клієнт, що й в App.tsx.
- Retry: Вимкніть retry. В тестах ви не хочете чекати 3 рази по 5 секунд, поки тест впаде.
- Cache: Очищайте кеш перед кожним тестом (а краще створюйте новий клієнт).
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.
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), тестуйте хук ізольовано.
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 для всіх асинхронних дій.
Server-Side Rendering (SSR) та Гідратація
Якщо ви використовуєте Next.js, Remix або Astro, ви хочете, щоб ваші дані завантажувалися на сервері для SEO та швидкого першого відтворення (LCP).
Аутентифікація та Обробка Помилок
Робота з захищеними даними (Authorized only) додає шар складності. Що робити, якщо токен протух? Як глобально показати помилку?