Vitest і React Testing Library чудово працюють разом
TL;DR: Якщо ви хочете просто побачити фінальний код і одразу з ним погратися, перейдіть сюди.
Чому тестування важливе
Якщо ви тут, напевно, вже розумієте важливість тестування. Для мене це означає забезпечення впевненості, що додавання нових функцій не порушить нічого у продуктивному середовищі, а також робить код більш гнучким для майбутніх змін. Це допомагає майбутнім розробникам зрозуміти, чому певні частини коду існують, і зручно додавати нові функції. Так, давайте піклуватися про тих, хто буде працювати з кодом у майбутньому! І так, вам все ще потрібно 100% покриття тестами.
Вступ
Нещодавно я налаштовував проєкт, який був налаштований на TypeScript, і мені було важко налаштувати Jest з TS. Тому я вирішив спробувати Vitest, і все спрацювало з першого разу.
Я знав, що API схоже на Jest, тому вирішив, що варто спробувати, оскільки відомо, що Vitest швидший.
Я помітив, що є деякі відмінності, можливо, найважливіша з яких полягає в тому, що в Jest автоматично мокуються всі модулі файлу, тоді як у Vitest потрібно вручну мокати кожен модуль, але це не стало великим бар'єром і я швидко до цього звик.
Припустимо, ви використовуєте axios
для виконання запитів. Ось код в Jest, який змокить всі методи axios, get, post тощо (підказка: ми ще повернемося до мокінгу axios):
jest.mock("axios");
В той час як у Vitest вам потрібно вручну змокати кожен метод:
vi.mock('axios', () => ({
default: {
get: vi.fn(),
},
}));
Jest і Vitest мають схожий спосіб мокати методи, але залишають інші незміненими (тобто використовують оригінальні методи). У Jest ви можете зробити так:
jest.mock('date-fns', () => {
const original = jest.requireActual('date-fns');
return {
...original, // Поширюємо оригінальний модуль
format: jest.fn(() => '2025-01-01'), // Мокаємо лише метод `format`
};
});
У Vitest ви можете зробити те ж саме:
vi.mock('date-fns', async () => {
const original = await vi.importActual('date-fns');
return {
...original, // Залишаємо всі оригінальні методи
format: vi.fn(() => '2025-01-01'), // Мокаємо `format`, щоб він повертав фіксоване значення
};
});
Додаток, який ми хочемо протестувати
Уявімо, що у нас є React додаток, який отримує пости з API та відображає їх у списку. Ось основний файл App.tsx
:
import { usePosts } from './hooks/usePosts';
import './styles.css';
export default function App() {
const { posts, loading, error } = usePosts();
if (loading) {
return
Loading...
; }
if (error) {
return
Error loading posts
;
}
return (
Posts list
{posts.map((post: any) => (
{post.title}
))}
);
}
Як ви помітили, ми використовуємо хук usePosts
для отримання постів. Давайте подивимося, як виглядає наш власний хук usePosts
:
import { useEffect, useState } from 'react';
import axios from 'axios';
import { Post } from '@/types/Post';
const API_URL = 'https://jsonplaceholder.typicode.com/posts';
export const usePosts = () => {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
useEffect(() => {
const fetchPosts = async () => {
try {
const response = await axios.get(API_URL);
setPosts(response.data);
} catch (error) {
setError(true);
} finally {
setLoading(false);
}
};
fetchPosts();
}, []);
return { posts, loading, error };
};
Тепер почнемо з тестування хука usePosts
. Це асинхронна функція, і ми використовуємо для цього axios
.
Спочатку ми хочемо протестувати щасливий шлях:
import { renderHook, waitFor } from '@testing-library/react';
import { usePosts } from './usePosts';
import axios from 'axios';
// Vitest не автоматично мокить модулі, як це робить Jest.
// Ми вручну мокимо 'axios', щоб контролювати його поведінку та використовуємо mockResolvedValue для тестування.
vi.mock('axios');
describe('usePosts', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('коли API виклик успішний', () => {
// Для інференції типу TypeScript використовуємо vi.mocked.
const mockAxios = vi.mocked(axios.get);
const mockPosts = [
{ id: 1, title: 'Post 1' },
{ id: 2, title: 'Post 2' },
];
beforeEach(() => {
// Ми використовуємо mockResolvedValue, щоб мокнути відповідь API.
mockAxios.mockResolvedValue({ data: mockPosts });
});
it('повинен повернути пости і встановити loading в false', async () => {
// Ми використовуємо renderHook з RTL для рендерингу нашого кастомного хуку.
const { result } = renderHook(() => usePosts());
// waitFor використовується для очікування асинхронних оновлень стану в хуку.
// Без цього твердження можуть виконатися до того, як API виклик завершиться.
await waitFor(() => {
expect(result.current.posts).toEqual(mockPosts);
expect(result.current.loading).toBe(false);
expect(result.current.error).toBe(false);
});
});
});
});
Мені подобається мокати все в beforeEach
, коли це можливо, оскільки я вважаю, що це робить твердження більш зрозумілими. Окрім цього, коментарі повинні допомогти зрозуміти, що відбувається.
Тепер нам потрібно змокати випадок, коли хук не повертає успішну відповідь, і перевіримо, чи буде змінено змінну error
на true
:
describe('коли API виклик не вдалий', () => {
const mockAxios = vi.mocked(axios.get);
beforeEach(() => {
mockAxios.mockRejectedValue(new Error('API виклик не вдався'));
});
it('повинен повернути порожній масив і встановити loading в false', async () => {
const { result } = renderHook(() => usePosts());
await waitFor(() => {
expect(result.current.posts).toEqual([]);
expect(result.current.loading).toBe(false);
expect(result.current.error).toBe(true);
});
});
});
Нарешті, ми протестуємо, як React компонент використовує цей кастомний хук. Пам'ятайте, що завжди потрібно мокати все, що ви не хочете тестувати. Наприклад, як ви побачите нижче, оскільки ми хочемо тестувати лише App.tsx
, ми змокуємо usePosts
. Давайте швидко пригадаємо, як виглядає App.tsx
:
import { usePosts } from './hooks/usePosts';
import './styles.css';
export default function App() {
const { posts, loading, error } = usePosts();
if (loading) {
return
Loading...
; }
if (error) {
return
Error loading posts
;
}
return (
Posts list
{posts.map((post: any) => (
{post.title}
))}
);
}
Спочатку давайте протестуємо стан завантаження цього компонента.
Це виглядатиме ось так:
import App from './App';
import { render } from '@testing-library/react';
import { usePosts } from '@/hooks/usePosts';
vi.mock('@/hooks/usePosts');
describe('App', () => {
const mockUsePosts = vi.mocked(usePosts);
beforeEach(() => {
vi.clearAllMocks();
});
describe('Стан завантаження', () => {
beforeEach(() => {
mockUsePosts.mockReturnValue({ posts: [], loading: true, error: false });
});
it('повинен відображати стан завантаження', () => {
const { getByText } = render();
expect(getByText('Loading...')).toBeInTheDocument();
});
});
});
Тепер давайте протестуємо, що відбудеться, коли хук повертає error
як true
, тобто коли завантаження постів не вдалося:
describe('Стан помилки', () => {
beforeEach(() => {
mockUsePosts.mockReturnValue({ posts: [], loading: false, error: true });
});
it('повинен відображати стан помилки', () => {
const { getByText } = render();
expect(getByText('Error loading posts')).toBeInTheDocument();
});
});
І, нарешті, стан успіху, коли все пройшло добре і ми просто відображаємо пости:
describe('Стан успіху', () => {
const mockPosts = [
{ id: 1, userId: 1, body: 'Content 1', title: 'Post 1' },
{ id: 2, userId: 2, body: 'Content 2', title: 'Post 2' },
];
beforeEach(() => {
mockUsePosts.mockReturnValue({
posts: mockPosts,
loading: false,
error: false,
});
});
it('повинен відображати пости', () => {
const { getByText } = render();
expect(getByText('Post 1')).toBeInTheDocument();
expect(getByText('Post 2')).toBeInTheDocument();
});
});
Висновок
Сподіваюсь, це дасть вам початкове уявлення про те, як використовувати Vitest для тестування React компонента разом з кастомним хуком. Ось посилання, якщо ви хочете дослідити або пограти з кодом:
- https://codesandbox.io/p/github/vaskort/vitest-blog-post/main?workspaceId=ws_AS8QkZMpvkUMLhEyFFNjkE
- https://github.com/vaskort/vitest-blog-post
Дякую за увагу!
Перекладено з: Bulletproof React Testing with Vitest & RTL