Testowanie aplikacji z Vitest i React Testing Library
Testowanie aplikacji Vitest
poradnikiTestowanie aplikacji z Vitest i React Testing Library
Testowanie to nieodlaczny element profesjonalnego wytwarzania oprogramowania. Dobrze napisane testy daja pewnosc, ze aplikacja dziala zgodnie z oczekiwaniami, chronia przed regresjami i ulatwiaja refaktoryzacje. W ekosystemie React przez lata dominowal Jest, ale Vitest szybko staje sie nowym standardem dzieki blyskawickiej szybkosci, natywnej obsludze ESM i doskonalej integracji z Vite. W tym przewodniku pokarzemy, jak skonfigurowac Vitest, pisac testy komponentow React z Testing Library, mockowac zaleznosci, testowac hooki, API i wiele wiecej.
Dlaczego Vitest zamiast Jest?#
Jest to sprawdzony framework testowy, ktory przez lata sluzyl spolecznosci React. Jednak Vitest oferuje kilka istotnych przewag, ktore sprawiaja, ze warto rozwazyc migracje.
Szybkosc#
Vitest wykorzystuje transformacje Vite (esbuild/SWC), co oznacza znacznie szybsza kompilacje TypeScript i JSX niz Babel uzywany przez Jest. Testy uruchamiaja sie niemal natychmiastowo, a tryb watch reaguje na zmiany w milisekundach.
Natywna obsluga ESM#
Vitest natywnie obsluguje ES Modules, co eliminuje problemy z konfiguracja transformIgnorePatterns i moduleNameMapper, ktore czesto sprawiaja klopoty w Jest.
Kompatybilnosc API#
API Vitest jest w duzej mierze kompatybilne z Jest. Funkcje takie jak describe, it, expect, vi.fn(), vi.mock() dzialaja analogicznie, co ulatwia migracje istniejacych testow.
Integracja z Vite#
Jesli Twoj projekt uzywa Vite jako bundlera, Vitest dzieli te sama konfiguracje. Nie musisz utrzymywac oddzielnej konfiguracji dla testow - aliasy, pluginy i transformacje sa wspoldzielone.
// vite.config.ts - konfiguracja wspoldzielona
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': '/src',
'@components': '/src/components',
'@utils': '/src/utils',
},
},
test: {
globals: true,
environment: 'jsdom',
setupFiles: './src/test/setup.ts',
css: true,
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
},
},
});
Konfiguracja projektu#
Zacznijmy od instalacji niezbednych zaleznosci.
Instalacja#
# Vitest i srodowisko DOM
npm install -D vitest @vitest/coverage-v8 jsdom
# React Testing Library
npm install -D @testing-library/react @testing-library/jest-dom @testing-library/user-event
# MSW do mockowania API
npm install -D msw
Plik setup#
Utworz plik konfiguracyjny, ktory bedzie ladowany przed kazdym plikiem testowym.
// src/test/setup.ts
import '@testing-library/jest-dom/vitest';
import { cleanup } from '@testing-library/react';
import { afterEach, vi } from 'vitest';
// Automatyczne czyszczenie po kazdym tescie
afterEach(() => {
cleanup();
});
// Mockowanie window.matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
// Mockowanie IntersectionObserver
class MockIntersectionObserver {
observe = vi.fn();
unobserve = vi.fn();
disconnect = vi.fn();
}
Object.defineProperty(window, 'IntersectionObserver', {
writable: true,
value: MockIntersectionObserver,
});
Skrypty w package.json#
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage",
"test:ui": "vitest --ui"
}
}
Testowanie komponentow React#
React Testing Library promuje testowanie komponentow z perspektywy uzytkownika. Zamiast testowac szczegoly implementacji, skupiamy sie na tym, co uzytkownik widzi i z czym wchodzi w interakcje.
Podstawowy test komponentu#
// src/components/Button/Button.tsx
interface ButtonProps {
label: string;
onClick: () => void;
disabled?: boolean;
variant?: 'primary' | 'secondary' | 'danger';
}
export function Button({ label, onClick, disabled = false, variant = 'primary' }: ButtonProps) {
return (
<button
className={`btn btn-${variant}`}
onClick={onClick}
disabled={disabled}
data-testid="custom-button"
>
{label}
</button>
);
}
// src/components/Button/Button.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi } from 'vitest';
import { Button } from './Button';
describe('Button', () => {
it('renderuje przycisk z podanym tekstem', () => {
render(<Button label="Kliknij mnie" onClick={() => {}} />);
expect(screen.getByRole('button', { name: 'Kliknij mnie' })).toBeInTheDocument();
});
it('wywoluje onClick po kliknieciu', async () => {
const handleClick = vi.fn();
const user = userEvent.setup();
render(<Button label="Zapisz" onClick={handleClick} />);
await user.click(screen.getByRole('button', { name: 'Zapisz' }));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('nie wywoluje onClick gdy przycisk jest zablokowany', async () => {
const handleClick = vi.fn();
const user = userEvent.setup();
render(<Button label="Zapisz" onClick={handleClick} disabled />);
await user.click(screen.getByRole('button', { name: 'Zapisz' }));
expect(handleClick).not.toHaveBeenCalled();
});
it('stosuje odpowiednia klase CSS dla wariantu', () => {
render(<Button label="Usun" onClick={() => {}} variant="danger" />);
expect(screen.getByRole('button')).toHaveClass('btn-danger');
});
});
Testowanie formularzy#
Formularze sa jednym z najczesciej testowanych elementow aplikacji. Testing Library dostarcza narzedzia do symulowania interakcji uzytkownika z polami formularzy.
// src/components/LoginForm/LoginForm.tsx
import { useState } from 'react';
interface LoginFormProps {
onSubmit: (credentials: { email: string; password: string }) => Promise<void>;
}
export function LoginForm({ onSubmit }: LoginFormProps) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
if (!email || !password) {
setError('Wypelnij wszystkie pola');
return;
}
setIsLoading(true);
try {
await onSubmit({ email, password });
} catch (err) {
setError('Nieprawidlowe dane logowania');
} finally {
setIsLoading(false);
}
};
return (
<form onSubmit={handleSubmit} aria-label="Formularz logowania">
{error && <div role="alert">{error}</div>}
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<label htmlFor="password">Haslo</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Logowanie...' : 'Zaloguj sie'}
</button>
</form>
);
}
// src/components/LoginForm/LoginForm.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi } from 'vitest';
import { LoginForm } from './LoginForm';
describe('LoginForm', () => {
const mockSubmit = vi.fn();
beforeEach(() => {
mockSubmit.mockReset();
});
it('wyswietla blad gdy pola sa puste', async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={mockSubmit} />);
await user.click(screen.getByRole('button', { name: 'Zaloguj sie' }));
expect(screen.getByRole('alert')).toHaveTextContent('Wypelnij wszystkie pola');
expect(mockSubmit).not.toHaveBeenCalled();
});
it('wysyla formularz z poprawnymi danymi', async () => {
const user = userEvent.setup();
mockSubmit.mockResolvedValueOnce(undefined);
render(<LoginForm onSubmit={mockSubmit} />);
await user.type(screen.getByLabelText('Email'), 'jan@example.com');
await user.type(screen.getByLabelText('Haslo'), 'tajnehaslo123');
await user.click(screen.getByRole('button', { name: 'Zaloguj sie' }));
await waitFor(() => {
expect(mockSubmit).toHaveBeenCalledWith({
email: 'jan@example.com',
password: 'tajnehaslo123',
});
});
});
it('wyswietla stan ladowania podczas wysylania', async () => {
const user = userEvent.setup();
mockSubmit.mockImplementation(() => new Promise(() => {}));
render(<LoginForm onSubmit={mockSubmit} />);
await user.type(screen.getByLabelText('Email'), 'jan@example.com');
await user.type(screen.getByLabelText('Haslo'), 'haslo123');
await user.click(screen.getByRole('button', { name: 'Zaloguj sie' }));
expect(screen.getByRole('button', { name: 'Logowanie...' })).toBeDisabled();
});
it('wyswietla blad przy nieudanym logowaniu', async () => {
const user = userEvent.setup();
mockSubmit.mockRejectedValueOnce(new Error('Unauthorized'));
render(<LoginForm onSubmit={mockSubmit} />);
await user.type(screen.getByLabelText('Email'), 'jan@example.com');
await user.type(screen.getByLabelText('Haslo'), 'zlehaslo');
await user.click(screen.getByRole('button', { name: 'Zaloguj sie' }));
await waitFor(() => {
expect(screen.getByRole('alert')).toHaveTextContent('Nieprawidlowe dane logowania');
});
});
});
Mockowanie z vi.mock i vi.fn#
Vitest udostepnia potezne narzedzia do mockowania modulow i funkcji. Dzieki nim mozesz izolowac testowany kod od zewnetrznych zaleznosci.
Mockowanie modulow#
// src/services/api.ts
export async function fetchUsers(): Promise<User[]> {
const response = await fetch('/api/users');
if (!response.ok) throw new Error('Failed to fetch users');
return response.json();
}
export async function createUser(data: CreateUserDto): Promise<User> {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!response.ok) throw new Error('Failed to create user');
return response.json();
}
// src/components/UserList/UserList.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { UserList } from './UserList';
import * as api from '@/services/api';
// Mockowanie calego modulu
vi.mock('@/services/api', () => ({
fetchUsers: vi.fn(),
}));
const mockFetchUsers = vi.mocked(api.fetchUsers);
describe('UserList', () => {
it('wyswietla liste uzytkownikow', async () => {
mockFetchUsers.mockResolvedValueOnce([
{ id: 1, name: 'Jan Kowalski', email: 'jan@example.com' },
{ id: 2, name: 'Anna Nowak', email: 'anna@example.com' },
]);
render(<UserList />);
expect(screen.getByText('Ladowanie...')).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText('Jan Kowalski')).toBeInTheDocument();
expect(screen.getByText('Anna Nowak')).toBeInTheDocument();
});
});
it('wyswietla blad gdy pobieranie sie nie powiedzie', async () => {
mockFetchUsers.mockRejectedValueOnce(new Error('Network error'));
render(<UserList />);
await waitFor(() => {
expect(screen.getByRole('alert')).toHaveTextContent('Nie udalo sie zaladowac uzytkownikow');
});
});
});
Mockowanie funkcji i szpiegowanie#
// Tworzenie mocka z implementacja
const mockCallback = vi.fn((x: number) => x * 2);
// Sprawdzanie wywolan
expect(mockCallback).toHaveBeenCalled();
expect(mockCallback).toHaveBeenCalledTimes(3);
expect(mockCallback).toHaveBeenCalledWith(5);
expect(mockCallback).toHaveReturnedWith(10);
// Szpiegowanie istniejacych metod
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
// ... test
expect(spy).toHaveBeenCalledWith('Something went wrong');
spy.mockRestore();
// Mockowanie timerow
vi.useFakeTimers();
setTimeout(() => callback(), 1000);
vi.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalled();
vi.useRealTimers();
Testy snapshotow#
Testy snapshotow sa przydatne do wykrywania nieoczekiwanych zmian w renderowanym HTML. Vitest obsluguje zarowno tradycyjne snapshoty plikowe, jak i inline snapshoty.
import { render } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { Card } from './Card';
describe('Card', () => {
it('renderuje poprawna strukture HTML', () => {
const { container } = render(
<Card title="Przykladowy tytul" description="Opis karty">
<p>Tresc karty</p>
</Card>
);
expect(container.firstChild).toMatchSnapshot();
});
it('renderuje inline snapshot', () => {
const { container } = render(<Card title="Test" />);
expect(container.firstChild).toMatchInlineSnapshot(`
<div class="card">
<h3 class="card-title">Test</h3>
</div>
`);
});
});
Pamietaj, ze snapshoty powinny byc uzywane z umiarem. Zbyt duze snapshoty staja sie trudne do przegladu i czesto sa bezrefleksyjnie aktualizowane, co zmniejsza ich wartosc diagnostyczna.
Pokrycie kodu (Coverage)#
Vitest uzywa providera v8 lub istanbul do mierzenia pokrycia kodu testami.
// vitest.config.ts
export default defineConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html', 'lcov'],
exclude: [
'node_modules/',
'src/test/',
'**/*.d.ts',
'**/*.config.*',
'**/types/**',
],
thresholds: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
},
});
Uruchom testy z pokryciem:
npx vitest run --coverage
Raport HTML zostanie wygenerowany w katalogu coverage/ i mozna go otworzyc w przegladarce.
Testowanie asynchroniczne#
Aplikacje React czesto wykonuja operacje asynchroniczne - pobieranie danych, timery, animacje. Vitest i Testing Library dostarczaja narzedzia do ich testowania.
// src/hooks/useDebounce.ts
import { useState, useEffect } from 'react';
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// src/components/SearchBox/SearchBox.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { SearchBox } from './SearchBox';
describe('SearchBox z debounce', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('wywoluje onSearch po uplywie czasu debounce', async () => {
const onSearch = vi.fn();
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
render(<SearchBox onSearch={onSearch} debounceMs={300} />);
await user.type(screen.getByRole('searchbox'), 'React');
// Przed uplywem debounce - nie powinno byc wywolania
expect(onSearch).not.toHaveBeenCalled();
// Po uplywie debounce
vi.advanceTimersByTime(300);
await waitFor(() => {
expect(onSearch).toHaveBeenCalledWith('React');
});
});
});
Mockowanie API z MSW#
Mock Service Worker (MSW) to doskonale narzedzie do mockowania API na poziomie sieci. Zamiast mockowac fetch czy axios, MSW przechwytuje zapytania HTTP i zwraca zdefiniowane odpowiedzi.
Konfiguracja MSW#
// src/test/mocks/handlers.ts
import { http, HttpResponse } from 'msw';
interface User {
id: number;
name: string;
email: string;
}
const users: User[] = [
{ id: 1, name: 'Jan Kowalski', email: 'jan@example.com' },
{ id: 2, name: 'Anna Nowak', email: 'anna@example.com' },
];
export const handlers = [
http.get('/api/users', () => {
return HttpResponse.json(users);
}),
http.get('/api/users/:id', ({ params }) => {
const user = users.find((u) => u.id === Number(params.id));
if (!user) {
return new HttpResponse(null, { status: 404 });
}
return HttpResponse.json(user);
}),
http.post('/api/users', async ({ request }) => {
const body = (await request.json()) as Omit<User, 'id'>;
const newUser: User = { id: Date.now(), ...body };
return HttpResponse.json(newUser, { status: 201 });
}),
http.delete('/api/users/:id', () => {
return new HttpResponse(null, { status: 204 });
}),
];
// src/test/mocks/server.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
Integracja z Vitest#
// src/test/setup.ts - dodaj konfiguracje MSW
import { server } from './mocks/server';
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
Uzycie w testach#
// src/components/UserProfile/UserProfile.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { http, HttpResponse } from 'msw';
import { server } from '@/test/mocks/server';
import { UserProfile } from './UserProfile';
describe('UserProfile', () => {
it('wyswietla dane uzytkownika', async () => {
render(<UserProfile userId={1} />);
await waitFor(() => {
expect(screen.getByText('Jan Kowalski')).toBeInTheDocument();
expect(screen.getByText('jan@example.com')).toBeInTheDocument();
});
});
it('wyswietla komunikat gdy uzytkownik nie istnieje', async () => {
server.use(
http.get('/api/users/:id', () => {
return new HttpResponse(null, { status: 404 });
})
);
render(<UserProfile userId={999} />);
await waitFor(() => {
expect(screen.getByText('Nie znaleziono uzytkownika')).toBeInTheDocument();
});
});
it('obsluguje blad serwera', async () => {
server.use(
http.get('/api/users/:id', () => {
return HttpResponse.error();
})
);
render(<UserProfile userId={1} />);
await waitFor(() => {
expect(screen.getByRole('alert')).toHaveTextContent('Blad ladowania danych');
});
});
});
Testowanie hookow#
Custom hooki mozna testowac za pomoca renderHook z React Testing Library.
// src/hooks/useLocalStorage.ts
import { useState, useCallback } from 'react';
export function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = useCallback(
(value: T | ((prev: T) => T)) => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
},
[key, storedValue]
);
const removeValue = useCallback(() => {
setStoredValue(initialValue);
window.localStorage.removeItem(key);
}, [key, initialValue]);
return [storedValue, setValue, removeValue] as const;
}
// src/hooks/useLocalStorage.test.ts
import { renderHook, act } from '@testing-library/react';
import { describe, it, expect, beforeEach } from 'vitest';
import { useLocalStorage } from './useLocalStorage';
describe('useLocalStorage', () => {
beforeEach(() => {
localStorage.clear();
});
it('zwraca wartosc poczatkowa gdy localStorage jest pusty', () => {
const { result } = renderHook(() => useLocalStorage('key', 'default'));
expect(result.current[0]).toBe('default');
});
it('odczytuje istniejaca wartosc z localStorage', () => {
localStorage.setItem('key', JSON.stringify('saved'));
const { result } = renderHook(() => useLocalStorage('key', 'default'));
expect(result.current[0]).toBe('saved');
});
it('aktualizuje wartosc w stanie i localStorage', () => {
const { result } = renderHook(() => useLocalStorage('key', 'initial'));
act(() => {
result.current[1]('updated');
});
expect(result.current[0]).toBe('updated');
expect(JSON.parse(localStorage.getItem('key')!)).toBe('updated');
});
it('obsluguje aktualizacje funkcyjna', () => {
const { result } = renderHook(() => useLocalStorage('count', 0));
act(() => {
result.current[1]((prev) => prev + 1);
});
expect(result.current[0]).toBe(1);
});
it('usuwa wartosc z localStorage', () => {
const { result } = renderHook(() => useLocalStorage('key', 'default'));
act(() => {
result.current[1]('value');
});
act(() => {
result.current[2]();
});
expect(result.current[0]).toBe('default');
expect(localStorage.getItem('key')).toBeNull();
});
});
Testy integracyjne#
Testy integracyjne weryfikuja wspoldzialanie wielu komponentow. Testuja realistyczne scenariusze uzycia bez mockowania wewnetrznych zaleznosci.
// src/features/TodoApp/TodoApp.test.tsx
import { render, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect } from 'vitest';
import { TodoApp } from './TodoApp';
describe('TodoApp - test integracyjny', () => {
it('pozwala dodawac, oznaczac i usuwac zadania', async () => {
const user = userEvent.setup();
render(<TodoApp />);
// Dodanie nowego zadania
const input = screen.getByPlaceholderText('Dodaj nowe zadanie...');
await user.type(input, 'Napisac testy{Enter}');
expect(screen.getByText('Napisac testy')).toBeInTheDocument();
expect(input).toHaveValue('');
// Dodanie drugiego zadania
await user.type(input, 'Przejrzec PR{Enter}');
const items = screen.getAllByRole('listitem');
expect(items).toHaveLength(2);
// Oznaczenie zadania jako wykonane
const firstItem = items[0];
const checkbox = within(firstItem).getByRole('checkbox');
await user.click(checkbox);
expect(checkbox).toBeChecked();
expect(firstItem).toHaveClass('completed');
// Usuniecie zadania
const deleteButton = within(firstItem).getByRole('button', { name: /usun/i });
await user.click(deleteButton);
expect(screen.queryByText('Napisac testy')).not.toBeInTheDocument();
expect(screen.getAllByRole('listitem')).toHaveLength(1);
});
it('filtruje zadania wedlug statusu', async () => {
const user = userEvent.setup();
render(<TodoApp />);
// Dodanie zadan
const input = screen.getByPlaceholderText('Dodaj nowe zadanie...');
await user.type(input, 'Zadanie 1{Enter}');
await user.type(input, 'Zadanie 2{Enter}');
// Oznacz pierwsze jako wykonane
const items = screen.getAllByRole('listitem');
await user.click(within(items[0]).getByRole('checkbox'));
// Filtruj aktywne
await user.click(screen.getByRole('button', { name: 'Aktywne' }));
expect(screen.getAllByRole('listitem')).toHaveLength(1);
expect(screen.getByText('Zadanie 2')).toBeInTheDocument();
// Filtruj wykonane
await user.click(screen.getByRole('button', { name: 'Wykonane' }));
expect(screen.getAllByRole('listitem')).toHaveLength(1);
expect(screen.getByText('Zadanie 1')).toBeInTheDocument();
});
});
Integracja z CI/CD#
Vitest doskonale integruje sie z popularnymi platformami CI/CD. Oto przykladowa konfiguracja dla GitHub Actions.
# .github/workflows/test.yml
name: Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20]
steps:
- uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests with coverage
run: npx vitest run --coverage --reporter=junit --outputFile=test-results.xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-${{ matrix.node-version }}
path: test-results.xml
Konfiguracja reporterow#
Vitest obsluguje rozne formaty raportow, przydatne w srodowiskach CI.
// vitest.config.ts
export default defineConfig({
test: {
reporters: ['default', 'junit', 'json'],
outputFile: {
junit: './test-results/junit.xml',
json: './test-results/results.json',
},
},
});
Najlepsze praktyki#
Stosowanie sprawdzonych praktyk testowania pozwoli Ci pisac testy, ktore sa latwe w utrzymaniu, czytelne i daja realna wartosc.
1. Testuj zachowanie, nie implementacje#
// Zle - testowanie szczegulow implementacji
expect(component.state.isOpen).toBe(true);
// Dobrze - testowanie z perspektywy uzytkownika
expect(screen.getByRole('dialog')).toBeVisible();
2. Uzywaj zapytan wedlug priorytetu#
React Testing Library zaleca nastepujaca kolejnosc zapytan:
getByRole- najbardziej dostepnegetByLabelText- dobre dla pol formularzygetByPlaceholderText- gdy brak labelagetByText- dla tresci tekstowejgetByTestId- ostatecznosc
3. Unikaj testowania szczegulow implementacji#
// Zle - poleganie na wewnetrznym stanie
const { result } = renderHook(() => useCounter());
expect(result.current.state.internalFlag).toBe(true);
// Dobrze - testowanie publicznego API
const { result } = renderHook(() => useCounter());
act(() => result.current.increment());
expect(result.current.count).toBe(1);
4. Organizuj testy wedlug AAA (Arrange, Act, Assert)#
it('dodaje produkt do koszyka', async () => {
// Arrange
const user = userEvent.setup();
render(<ProductPage productId="123" />);
await waitFor(() => expect(screen.getByText('Produkt testowy')).toBeInTheDocument());
// Act
await user.click(screen.getByRole('button', { name: 'Dodaj do koszyka' }));
// Assert
expect(screen.getByText('Dodano do koszyka')).toBeInTheDocument();
});
5. Tworzenie helperow do renderowania#
// src/test/utils.tsx
import { render, RenderOptions } from '@testing-library/react';
import { ReactElement } from 'react';
import { BrowserRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ThemeProvider } from '@/contexts/ThemeContext';
function createTestQueryClient() {
return new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
}
interface CustomRenderOptions extends Omit<RenderOptions, 'wrapper'> {
queryClient?: QueryClient;
}
export function renderWithProviders(
ui: ReactElement,
{ queryClient = createTestQueryClient(), ...options }: CustomRenderOptions = {}
) {
function Wrapper({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<ThemeProvider>{children}</ThemeProvider>
</BrowserRouter>
</QueryClientProvider>
);
}
return {
...render(ui, { wrapper: Wrapper, ...options }),
queryClient,
};
}
export { screen, waitFor, within } from '@testing-library/react';
export { default as userEvent } from '@testing-library/user-event';
6. Uzywaj fabryk danych testowych#
// src/test/factories.ts
interface UserFactory {
id: number;
name: string;
email: string;
role: 'admin' | 'user';
}
let nextId = 1;
export function createUser(overrides: Partial<UserFactory> = {}): UserFactory {
return {
id: nextId++,
name: `User ${nextId}`,
email: `user${nextId}@example.com`,
role: 'user',
...overrides,
};
}
export function createUsers(count: number, overrides: Partial<UserFactory> = {}): UserFactory[] {
return Array.from({ length: count }, () => createUser(overrides));
}
Podsumowanie#
Vitest w polaczeniu z React Testing Library tworzy potezny zestaw narzedzi do testowania aplikacji React. Kluczowe wnioski z tego przewodnika:
- Vitest oferuje blyskawicka szybkosc, natywna obsluge ESM i doskonala integracje z Vite
- React Testing Library promuje testowanie z perspektywy uzytkownika, co prowadzi do bardziej wartosciowych testow
- MSW pozwala realistycznie mockowac API na poziomie sieci
- Testy hookow mozna pisac za pomoca
renderHookbez tworzenia komponentow opakowujacych - Testy integracyjne weryfikuja realistyczne scenariusze uzycia
- CI/CD integracja zapewnia automatyczne uruchamianie testow przy kazdej zmianie
Pamietaj, ze testy powinny dawac pewnosc, ze aplikacja dziala poprawnie, a nie byc ciezarem. Skupiaj sie na testowaniu kluczowych scenariuszy biznesowych i krytycznych sciezek uzytkownika.
Potrzebujesz pomocy w budowie dobrze przetestowanej aplikacji React? Zespol MDS Software Solutions Group specjalizuje sie w tworzeniu niezawodnych aplikacji webowych z kompleksowym pokryciem testami. Skontaktuj sie z nami i porozmawiajmy o Twoim projekcie.
Zespół ekspertów programistycznych specjalizujących się w nowoczesnych technologiach webowych.