Przejdź do treści
Poradniki

Testowanie aplikacji z Vitest i React Testing Library

Opublikowano:
·4 min czytania·Autor: MDS Software Solutions Group

Testowanie aplikacji Vitest

poradniki

Testowanie 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:

  1. getByRole - najbardziej dostepne
  2. getByLabelText - dobre dla pol formularzy
  3. getByPlaceholderText - gdy brak labela
  4. getByText - dla tresci tekstowej
  5. getByTestId - 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 renderHook bez 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.

Autor
MDS Software Solutions Group

Zespół ekspertów programistycznych specjalizujących się w nowoczesnych technologiach webowych.

Testowanie aplikacji z Vitest i React Testing Library | MDS Software Solutions Group | MDS Software Solutions Group