Надійне тестування React з Vitest та RTL

pic

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 компонента разом з кастомним хуком. Ось посилання, якщо ви хочете дослідити або пограти з кодом:

Дякую за увагу!

Перекладено з: Bulletproof React Testing with Vitest & RTL

Leave a Reply

Your email address will not be published. Required fields are marked *