Paweł Łukasiewicz: programista blogger
Paweł Łukasiewicz
2026-02-03
Paweł Łukasiewicz: programista blogger
Paweł Łukasiewicz
2026-02-03
Udostępnij Udostępnij Kontakt
Wprowadzenie

Aplikacja działa, funkcje są zaimplementowane, użytkownicy są zadowoleni. Ale nagle po drobnej zmianie w kodzie... coś przestaje działać. Bug na produkcji. Użytkownicy narzekają. Rollback. Stres. To mogło być uniknięte z jedną rzeczą: testami.

🔍 Dlaczego testy są ważne?

Wyobraź sobie że piszesz kod jak budujesz dom. Bez testów:

  • ❌ Dodajesz nowy pokój (feature) → ściana w salonie pęka (stary kod się psuje)
  • ❌ Nie wiesz co się zepsuło dopóki użytkownicy nie zaczną narzekać
  • ❌ Boisz się zmieniać cokolwiek (co jeśli coś zepsuje?)

Z testami:

  • ✅ Każda zmiana jest sprawdzana automatycznie
  • ✅ Dowiadujesz się o problemach OD RAZU (nie na produkcji!)
  • ✅ Możesz refaktoryzować bez strachu - testy Cię chronią

Testy to nie opcja – to konieczność w profesjonalnych projektach. Dają pewność, że kod działa poprawnie, pozwalają na bezpieczny refactoring, dokumentują jak komponenty powinny działać, i zwiększają szybkość developmentu (tak, naprawdę!). Bez testów żyjesz w strachu przed każdym deployem.

🔍 "Testy zwiększają szybkość?" - Jak to?

Początkowo wydaje się że testy spowalniają - musisz je napisać! Ale:

  1. Bez testów: Zmieniasz kod → odpalasz całą aplikację → klikasz przez 10 ekranów → sprawdzasz czy działa → znajdujesz bug → naprawiasz → powtarzasz. 20 minut.
  2. Z testami: Zmieniasz kod → npm test → widzisz wyniki w 2 sekundy. 2 sekundy.

Po kilku miesiącach projektu oszczędzasz SETKI godzin! 🚀

W tym wpisie nauczymy się testować aplikacje React + TypeScript. Poznamy Testing Library, Jest, Vitest, nauczymy się testować komponenty, hooki, integracje, async code, mocks. Zbudujemy kompletną strategię testowania. To będzie praktyczny wpis pełen przykładów – po nim będziesz pisał testy jak profesjonalista!

Rodzaje testów

Piramida testów

🔍 Co to jest piramida testów?

To strategia mówiąca ile testów jakiego typu powinieneś mieć. Im wyżej w piramidzie, tym:

  • Wolniejsze wykonanie
  • Droższe w utrzymaniu (łatwiej się psują)
  • Testują więcej kodu naraz

Zasada: Dużo szybkich testów u podstawy, mało wolnych na górze!

      /\
     /  \    E2E (End-to-End)
    /----\   - Wolne, kosztowne
   /      \  - Testują całą aplikację
  /--------\ - Przykład: Cypress, Playwright
 /          \
/------------\ Integration
|            | - Średnie tempo
|            | - Testują współpracę komponentów
|____________| - Przykład: Testing Library
|            |
|            | Unit
|            | - Szybkie, tanie
|            | - Testują pojedyncze funkcje/komponenty
|____________| - Przykład: Jest, Vitest

Zasada: Najwięcej unit testów, średnio integration, najmniej E2E.

🌟 Przykłady każdego typu testu:

1. Unit Test - testuje jedną funkcję/komponent:

// Testuje tylko funkcję formatPrice
test('formatPrice formatuje cenę poprawnie', () => {
  expect(formatPrice(1234.56)).toBe('1 234,56 zł');
});

2. Integration Test - testuje współpracę kilku komponentów:

// Testuje TodoList + TodoItem + AddTodo razem
test('dodanie todo pokazuje je na liście', async () => {
  render();
  await user.type(screen.getByRole('textbox'), 'Kupić mleko');
  await user.click(screen.getByText('Dodaj'));
  expect(screen.getByText('Kupić mleko')).toBeInTheDocument();
});

3. E2E Test - testuje całą aplikację jak prawdziwy użytkownik:

// Testuje cały flow: login → dodaj produkt → checkout
test('użytkownik może kupić produkt', async () => {
  await page.goto('http://localhost:3000');
  await page.click('text=Zaloguj się');
  await page.fill('input[name="email"]', 'user@test.com');
  await page.fill('input[name="password"]', 'password123');
  await page.click('button[type="submit"]');
  await page.click('text=Produkty');
  await page.click('text=Dodaj do koszyka');
  await page.click('text=Koszyk');
  await page.click('text=Zamów i zapłać');
  await expect(page.locator('text=Zamówienie złożone')).toBeVisible();
});

Czego testujemy w React?

  1. Unit tests – pojedyncze funkcje, utility, hooki
  2. Component tests – komponenty w izolacji
  3. Integration tests – współpraca komponentów
  4. E2E tests – cała aplikacja (user flow)

W tym wpisie skupimy się na 1-3.

🔍 Co konkretnie testować?

✅ TESTUJ:

  • Czy komponent się renderuje?
  • Czy przycisk wywołuje funkcję po kliknięciu?
  • Czy formularz waliduje dane poprawnie?
  • Czy dane z API się wyświetlają?
  • Czy komunikaty błędów się pokazują?

❌ NIE TESTUJ:

  • Szczegółów implementacji (np. czy komponent używa useState czy useReducer)
  • Bibliotek zewnętrznych (React, Zustand - one mają swoje testy!)
  • Stylów CSS (czy przycisk jest niebieski - to nie test, to wizualna inspekcja)
💪 Ćwiczenie 1: Zaplanuj testy

Masz komponent LoginForm z polami email, hasło i przyciskiem "Zaloguj".

Wymyśl 5 testów które powinieneś napisać. Przykład pierwszego:

  1. "Formularz renderuje się z dwoma polami input i przyciskiem"
  2. ...twoje pomysły...
Setup testowego środowiska
🔍 Co musimy zainstalować?
  • Vitest - runner testów (uruchamia testy, sprawdza asercje)
  • @testing-library/react - renderuje komponenty React w testach
  • @testing-library/jest-dom - dodatkowe matchery (toBeInTheDocument, toBeDisabled)
  • @testing-library/user-event - symuluje interakcje użytkownika (klikanie, pisanie)
  • jsdom - symuluje przeglądarkę w Node.js (żeby testy mogły "renderować" HTML)

Vite + Vitest (polecam!)

npm install -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: './src/test/setup.ts',
  },
});
// src/test/setup.ts
import '@testing-library/jest-dom';
// package.json
{
  "scripts": {
    "test": "vitest",
    "test:ui": "vitest --ui",
    "test:coverage": "vitest --coverage"
  }
}
🔍 Co robią te komendy?
  • npm test - uruchamia testy w watch mode (automatycznie po zmianach)
  • npm run test:ui - otwiera wizualny interfejs w przeglądarce (bardzo fajny!)
  • npm run test:coverage - pokazuje % pokrycia kodu testami

Create React App (legacy)

CRA ma już skonfigurowany Jest + Testing Library:

npm test
Podstawy testowania

Pierwszy test

🔍 Anatomia testu:
  1. describe - grupuje powiązane testy (opcjonalny)
  2. test (lub it) - pojedynczy test
  3. expect - sprawdza czy coś jest prawdą (asercja)
// utils/sum.test.ts
import { describe, test, expect } from 'vitest';

function sum(a: number, b: number): number {
  return a + b;
}

describe('sum', () => {
  test('adds 1 + 2 to equal 3', () => {
    expect(sum(1, 2)).toBe(3);
  });

  test('adds negative numbers', () => {
    expect(sum(-1, -2)).toBe(-3);
  });
});

Uruchom: npm test

🌟 Przykład: Testowanie funkcji pomocniczych
// utils/format.ts
export function formatPrice(price: number): string {
  return new Intl.NumberFormat('pl-PL', {
    style: 'currency',
    currency: 'PLN'
  }).format(price);
}

export function truncateText(text: string, maxLength: number): string {
  if (text.length <= maxLength) return text;
  return text.slice(0, maxLength) + '...';
}

// utils/format.test.ts
import { describe, test, expect } from 'vitest';
import { formatPrice, truncateText } from './format';

describe('formatPrice', () => {
  test('formatuje cenę w PLN', () => {
    expect(formatPrice(1234.56)).toBe('1 234,56 zł');
  });

  test('formatuje 0 jako 0,00 zł', () => {
    expect(formatPrice(0)).toBe('0,00 zł');
  });

  test('formatuje liczby ujemne', () => {
    expect(formatPrice(-100)).toBe('-100,00 zł');
  });
});

describe('truncateText', () => {
  test('nie skraca tekstu krótszego niż limit', () => {
    expect(truncateText('Hello', 10)).toBe('Hello');
  });

  test('skraca długi tekst i dodaje ...', () => {
    expect(truncateText('Hello World', 5)).toBe('Hello...');
  });

  test('obsługuje pusty string', () => {
    expect(truncateText('', 5)).toBe('');
  });
});

Proste funkcje = proste testy! To najłatwiejszy typ testów do napisania. 🎯

Matchers (assertions)

🔍 Co to są matchers?

Matchers (dopasowywacze) to funkcje które sprawdzają czy wartość jest taka jak oczekujesz:

  • expect(value).toBe(5) - sprawdza czy value === 5
  • expect(value).not.toBe(5) - sprawdza czy value !== 5

Jest ich dziesiątki - dla każdego typu danych!

// Equality
expect(value).toBe(5);           // ===
expect(value).toEqual({ a: 1 }); // deep equality

// Truthiness
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeUndefined();
expect(value).toBeDefined();

// Numbers
expect(value).toBeGreaterThan(3);
expect(value).toBeLessThan(5);
expect(value).toBeCloseTo(0.3); // dla floatów

// Strings
expect(value).toMatch(/pattern/);
expect(value).toContain('substring');

// Arrays
expect(array).toContain(item);
expect(array).toHaveLength(3);

// Objects
expect(obj).toHaveProperty('key');
expect(obj).toMatchObject({ a: 1 });
🔍 toBe vs toEqual - jaka różnica?

toBe - sprawdza czy to TEN SAM obiekt w pamięci (===):

const obj1 = { a: 1 };
const obj2 = { a: 1 };
expect(obj1).toBe(obj2); // ❌ FAIL - to różne obiekty!
expect(obj1).toBe(obj1); // ✅ PASS - ten sam obiekt

toEqual - sprawdza czy mają TĘ SAMĄ zawartość (deep equality):

const obj1 = { a: 1 };
const obj2 = { a: 1 };
expect(obj1).toEqual(obj2); // ✅ PASS - ta sama zawartość!

Reguła: Dla prymitywów (number, string, boolean) - toBe. Dla obiektów/tablic - toEqual.

💪 Ćwiczenie 2: Napisz testy dla funkcji

Napisz testy dla tej funkcji:

function isValidEmail(email: string): boolean {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

Napisz przynajmniej 5 testów sprawdzających:

  1. Poprawny email (test@example.com) → true
  2. Email bez @ → false
  3. Email bez domeny → false
  4. Pusty string → false
  5. Email ze spacjami → false
Testowanie komponentów

Prosty komponent

🔍 Jak testować komponenty React?

Testowanie komponentów to 3 kroki:

  1. render() - renderuj komponent w testowym środowisku
  2. screen.getBy...() - znajdź element na ekranie
  3. expect() - sprawdź czy element jest taki jak powinien
// Button.tsx
interface ButtonProps {
  label: string;
  onClick: () => void;
  disabled?: boolean;
}

function Button({ label, onClick, disabled }: ButtonProps) {
  return (
    <button onClick={onClick} disabled={disabled}>
      {label}
    </button>
  );
}

export default Button;
// Button.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, test, expect, vi } from 'vitest';
import Button from './Button';

describe('Button', () => {
  test('renders with label', () => {
    render(<Button label="Click me" onClick={() => {}} />);
    
    expect(screen.getByText('Click me')).toBeInTheDocument();
  });

  test('calls onClick when clicked', async () => {
    const handleClick = vi.fn();
    const user = userEvent.setup();
    
    render(<Button label="Click me" onClick={handleClick} />);
    
    await user.click(screen.getByText('Click me'));
    
    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  test('is disabled when disabled prop is true', () => {
    render(<Button label="Click me" onClick={() => {}} disabled />);
    
    expect(screen.getByText('Click me')).toBeDisabled();
  });
});
🔍 Co to jest vi.fn()?

vi.fn() tworzy mock function - fałszywą funkcję która:

  • ✅ Pamięta ile razy została wywołana
  • ✅ Pamięta z jakimi argumentami
  • ✅ Może zwrócić dowolną wartość

Po co? Żeby sprawdzić czy komponent wywołuje callback'i poprawnie!

const mockFn = vi.fn();
mockFn('hello', 123);
mockFn('world');

expect(mockFn).toHaveBeenCalledTimes(2);        // Wywołano 2 razy
expect(mockFn).toHaveBeenCalledWith('hello', 123); // Pierwszy call
expect(mockFn).toHaveBeenLastCalledWith('world'); // Ostatni call

Queries (znajdowanie elementów)

🔍 Jak znaleźć element w teście?

Jest wiele sposobów - wybieraj od najbardziej dostępnego (dla screen readerów):

// Priorytet queries (od najlepszego):

// 1. getByRole (najlepszy - accessibility)
screen.getByRole('button', { name: /submit/i });
screen.getByRole('textbox', { name: /email/i });

// 2. getByLabelText (dla formularzy)
screen.getByLabelText('Email');

// 3. getByPlaceholderText
screen.getByPlaceholderText('Enter email');

// 4. getByText
screen.getByText('Hello World');
screen.getByText(/hello/i); // regex, case insensitive

// 5. getByDisplayValue (wartość input)
screen.getByDisplayValue('Current value');

// 6. getByAltText (dla obrazków)
screen.getByAltText('Profile picture');

// 7. getByTitle
screen.getByTitle('Close');

// 8. getByTestId (ostateczność!)
screen.getByTestId('custom-element');
🌟 Przykład: Dlaczego getByRole jest najlepszy?

getByRole szuka elementów tak jak screen reader - jeśli Twój test działa, to aplikacja jest dostępna dla niepełnosprawnych!

// ✅ Dobry test - dostępny dla screen readerów
render(<button>Submit</button>);
screen.getByRole('button', { name: 'Submit' });

// ❌ Zły test - nie sprawdza dostępności
render(<div onClick={handleClick}>Submit</div>);
screen.getByText('Submit'); // Działa, ale to NIE jest przycisk dla screen readera!

Role dla różnych elementów:

  • <button> → role="button"
  • <input type="text"> → role="textbox"
  • <input type="checkbox"> → role="checkbox"
  • <select> → role="combobox"
  • <a> → role="link"
  • <h1> → role="heading"

Warianty queries

🔍 getBy vs queryBy vs findBy - jaka różnica?
  • getBy - rzuca błąd jeśli nie znajdzie (dla elementów które MUSZĄ być)
  • queryBy - zwraca null jeśli nie znajdzie (dla sprawdzenia że czegoś NIE MA)
  • findBy - async, czeka na element (dla elementów które pojawią się po chwili)
// getBy - rzuca błąd jeśli nie znajdzie (1 element)
screen.getByText('Hello');

// queryBy - zwraca null jeśli nie znajdzie (dla sprawdzenia braku elementu)
expect(screen.queryByText('Hello')).not.toBeInTheDocument();

// findBy - async, czeka na element (dla async rendering)
const element = await screen.findByText('Hello');

// getAllBy, queryAllBy, findAllBy - dla wielu elementów
const buttons = screen.getAllByRole('button');
🌟 Kiedy użyć którego?
// ✅ getBy - element JEST i nie zmienia się
test('button is visible', () => {
  render(<Button label="Click" />);
  expect(screen.getByRole('button')).toBeInTheDocument();
});

// ✅ queryBy - sprawdzam że elementu NIE MA
test('error is not shown initially', () => {
  render(<Form />);
  expect(screen.queryByText(/error/i)).not.toBeInTheDocument();
});

// ✅ findBy - element pojawi się po chwili (async)
test('user data loads', async () => {
  render(<UserProfile userId={1} />);
  expect(await screen.findByText('John Doe')).toBeInTheDocument();
});
💪 Ćwiczenie 3: Testowanie komponentu Counter

Masz komponent:

function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(count - 1)}>Decrement</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}

Napisz 5 testów:

  1. Początkowa wartość to 0
  2. Increment zwiększa o 1
  3. Decrement zmniejsza o 1
  4. Reset ustawia na 0
  5. Wielokrotne kliknięcia działają poprawnie (np. 3x increment = 3)
User interactions

userEvent (polecam!)

🔍 userEvent vs fireEvent - jaka różnica?

userEvent - symuluje PRAWDZIWEGO użytkownika:

  • Wpisywanie tekstu → każda litera osobno, z opóźnieniem
  • Kliknięcie → hover + mousedown + mouseup + click
  • Tab → focus na następny element

fireEvent - wywołuje event BEZPOŚREDNIO (szybciej, ale mniej realistycznie)

Polecam userEvent! Bliższy prawdziwym interakcjom.

import userEvent from '@testing-library/user-event';

test('user interactions', async () => {
  const user = userEvent.setup();
  render(<MyComponent />);

  // Click
  await user.click(screen.getByRole('button'));

  // Type
  await user.type(screen.getByRole('textbox'), 'Hello');

  // Clear
  await user.clear(screen.getByRole('textbox'));

  // Select option
  await user.selectOptions(screen.getByRole('combobox'), 'option1');

  // Check checkbox
  await user.click(screen.getByRole('checkbox'));

  // Keyboard
  await user.keyboard('{Enter}');
  await user.keyboard('{Escape}');

  // Tab navigation
  await user.tab();
});

fireEvent (dla prostych przypadków)

import { fireEvent } from '@testing-library/react';

test('simple events', () => {
  render(<MyComponent />);

  fireEvent.click(screen.getByRole('button'));
  fireEvent.change(screen.getByRole('textbox'), { 
    target: { value: 'Hello' } 
  });
  fireEvent.submit(screen.getByRole('form'));
});
Testowanie formularzy

🔍 Co testować w formularzach?
  • ✅ Czy można wpisać dane?
  • ✅ Czy walidacja działa?
  • ✅ Czy submit wysyła poprawne dane?
  • ✅ Czy błędy się pokazują?
// LoginForm.tsx
interface LoginFormProps {
  onSubmit: (email: string, password: string) => void;
}

function LoginForm({ onSubmit }: LoginFormProps) {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    onSubmit(email, password);
  }

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="email">Email</label>
      <input
        id="email"
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />

      <label htmlFor="password">Password</label>
      <input
        id="password"
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />

      <button type="submit">Login</button>
    </form>
  );
}
// LoginForm.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, test, expect, vi } from 'vitest';
import LoginForm from './LoginForm';

describe('LoginForm', () => {
  test('submits form with email and password', async () => {
    const handleSubmit = vi.fn();
    const user = userEvent.setup();

    render(<LoginForm onSubmit={handleSubmit} />);

    // Wpisz email
    await user.type(screen.getByLabelText(/email/i), 'test@example.com');

    // Wpisz hasło
    await user.type(screen.getByLabelText(/password/i), 'password123');

    // Kliknij submit
    await user.click(screen.getByRole('button', { name: /login/i }));

    // Sprawdź czy onSubmit został wywołany z poprawnymi danymi
    expect(handleSubmit).toHaveBeenCalledWith('test@example.com', 'password123');
  });

  test('inputs are initially empty', () => {
    render(<LoginForm onSubmit={() => {}} />);

    expect(screen.getByLabelText(/email/i)).toHaveValue('');
    expect(screen.getByLabelText(/password/i)).toHaveValue('');
  });
});
🌟 Przykład: Formularz z walidacją
function SignupForm({ onSubmit }) {
  const [email, setEmail] = useState('');
  const [error, setError] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (!email.includes('@')) {
      setError('Invalid email');
      return;
    }
    setError('');
    onSubmit(email);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input 
        type="email" 
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        aria-label="Email"
      />
      {error && <p role="alert">{error}</p>}
      <button type="submit">Sign up</button>
    </form>
  );
}

// Test
test('shows error for invalid email', async () => {
  const user = userEvent.setup();
  render(<SignupForm onSubmit={vi.fn()} />);

  await user.type(screen.getByLabelText('Email'), 'invalid');
  await user.click(screen.getByRole('button', { name: 'Sign up' }));

  expect(screen.getByRole('alert')).toHaveTextContent('Invalid email');
});

test('submits for valid email', async () => {
  const handleSubmit = vi.fn();
  const user = userEvent.setup();
  render(<SignupForm onSubmit={handleSubmit} />);

  await user.type(screen.getByLabelText('Email'), 'test@example.com');
  await user.click(screen.getByRole('button', { name: 'Sign up' }));

  expect(handleSubmit).toHaveBeenCalledWith('test@example.com');
  expect(screen.queryByRole('alert')).not.toBeInTheDocument();
});
💪 Ćwiczenie 4: Test formularza kontaktowego

Formularz ma: imię, email, wiadomość (textarea), checkbox "Zgadzam się".

Napisz testy dla:

  1. Wszystkie pola są puste na początku
  2. Można wpisać tekst w każde pole
  3. Checkbox musi być zaznaczony żeby wysłać
  4. Submit wywołuje callback z poprawnymi danymi
  5. Błąd gdy email nie ma @
Testowanie async code

🔍 Dlaczego async jest trudniejszy?

Kod asynchroniczny (API calls, timeouty) dzieje się "w tle". Test musi POCZEKAĆ na wynik!

Narzędzia:

  • await screen.findBy...() - czeka na element (max 1s)
  • waitFor(() => {...}) - czeka aż warunek będzie spełniony
  • vi.fn() - mock fetch/API
// UserProfile.tsx
interface User {
  id: number;
  name: string;
  email: string;
}

function UserProfile({ userId }: { userId: number }) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    async function fetchUser() {
      try {
        const response = await fetch(`/api/users/${userId}`);
        const data = await response.json();
        setUser(data);
      } catch (err) {
        setError('Failed to load user');
      } finally {
        setLoading(false);
      }
    }
    fetchUser();
  }, [userId]);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!user) return null;

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}
// UserProfile.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import { describe, test, expect, vi, beforeEach } from 'vitest';
import UserProfile from './UserProfile';

// Mock global fetch
global.fetch = vi.fn();

describe('UserProfile', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  test('shows loading state initially', () => {
    (global.fetch as any).mockResolvedValue({
      json: async () => ({ id: 1, name: 'John', email: 'john@example.com' })
    });

    render(<UserProfile userId={1} />);

    expect(screen.getByText(/loading/i)).toBeInTheDocument();
  });

  test('displays user data after loading', async () => {
    (global.fetch as any).mockResolvedValue({
      json: async () => ({ id: 1, name: 'John Doe', email: 'john@example.com' })
    });

    render(<UserProfile userId={1} />);

    // Czekaj aż załaduje się user
    await waitFor(() => {
      expect(screen.getByText('John Doe')).toBeInTheDocument();
    });

    expect(screen.getByText('john@example.com')).toBeInTheDocument();
  });

  test('displays error when fetch fails', async () => {
    (global.fetch as any).mockRejectedValue(new Error('Network error'));

    render(<UserProfile userId={1} />);

    await waitFor(() => {
      expect(screen.getByText(/error/i)).toBeInTheDocument();
    });
  });

  test('fetches new user when userId changes', async () => {
    (global.fetch as any)
      .mockResolvedValueOnce({
        json: async () => ({ id: 1, name: 'User 1', email: 'user1@example.com' })
      })
      .mockResolvedValueOnce({
        json: async () => ({ id: 2, name: 'User 2', email: 'user2@example.com' })
      });

    const { rerender } = render(<UserProfile userId={1} />);

    await waitFor(() => {
      expect(screen.getByText('User 1')).toBeInTheDocument();
    });

    // Zmień userId
    rerender(<UserProfile userId={2} />);

    await waitFor(() => {
      expect(screen.getByText('User 2')).toBeInTheDocument();
    });

    expect(global.fetch).toHaveBeenCalledTimes(2);
  });
});
🔍 Mock fetch - krok po kroku:
  1. global.fetch = vi.fn() - zastępujemy prawdziwy fetch mockiem
  2. .mockResolvedValue({...}) - mówimy co fetch ma zwrócić (sukces)
  3. .mockRejectedValue(...) - mówimy że fetch ma zwrócić błąd
  4. vi.clearAllMocks() - czyścimy historię między testami

Po co mockować? Żeby NIE wysyłać prawdziwych requestów! Testy muszą być:

  • ✅ Szybkie (prawdziwe API = wolne)
  • ✅ Niezawodne (API może być offline)
  • ✅ Powtarzalne (zawsze ten sam wynik)
🌟 Przykład: findBy vs waitFor
// ✅ findBy - prostsza składnia dla pojedynczego elementu
test('user loads', async () => {
  render(<UserProfile userId={1} />);
  expect(await screen.findByText('John Doe')).toBeInTheDocument();
});

// ✅ waitFor - gdy musisz sprawdzić więcej rzeczy
test('user loads with email', async () => {
  render(<UserProfile userId={1} />);
  await waitFor(() => {
    expect(screen.getByText('John Doe')).toBeInTheDocument();
    expect(screen.getByText('john@example.com')).toBeInTheDocument();
  });
});
Testowanie custom hooks

🔍 Dlaczego hooki testujemy inaczej?

Hooki NIE MOGĄ być wywołane poza komponentem. Nie możesz zrobić:

// ❌ To nie zadziała!
const result = useCounter();

Rozwiązanie: renderHook - "udaje" że hook jest w komponencie!

// useCounter.ts
import { useState } from 'react';

export function useCounter(initialValue: number = 0) {
  const [count, setCount] = useState(initialValue);

  const increment = () => setCount(c => c + 1);
  const decrement = () => setCount(c => c - 1);
  const reset = () => setCount(initialValue);

  return { count, increment, decrement, reset };
}
// useCounter.test.ts
import { renderHook, act } from '@testing-library/react';
import { describe, test, expect } from 'vitest';
import { useCounter } from './useCounter';

describe('useCounter', () => {
  test('initializes with default value', () => {
    const { result } = renderHook(() => useCounter());
    expect(result.current.count).toBe(0);
  });

  test('initializes with custom value', () => {
    const { result } = renderHook(() => useCounter(10));
    expect(result.current.count).toBe(10);
  });

  test('increments count', () => {
    const { result } = renderHook(() => useCounter());

    act(() => {
      result.current.increment();
    });

    expect(result.current.count).toBe(1);
  });

  test('decrements count', () => {
    const { result } = renderHook(() => useCounter(5));

    act(() => {
      result.current.decrement();
    });

    expect(result.current.count).toBe(4);
  });

  test('resets to initial value', () => {
    const { result } = renderHook(() => useCounter(10));

    act(() => {
      result.current.increment();
      result.current.increment();
      result.current.reset();
    });

    expect(result.current.count).toBe(10);
  });
});
🔍 Co to jest act()?

act() mówi React: "wykonaj teraz wszystkie aktualizacje stanu". Bez tego:

// ❌ Bez act - test może nie zadziałać
result.current.increment();
expect(result.current.count).toBe(1); // Może być jeszcze 0!

// ✅ Z act - czekamy na aktualizację
act(() => {
  result.current.increment();
});
expect(result.current.count).toBe(1); // Na pewno 1!

Reguła: Zawsze owijaj wywołania funkcji zmieniających stan w act()!

🌟 Przykład: Hook z async logiką
// useFetch.ts
export function useFetch(url: string) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function fetchData() {
      try {
        const response = await fetch(url);
        const json = await response.json();
        setData(json);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    }
    fetchData();
  }, [url]);

  return { data, loading, error };
}

// useFetch.test.ts
global.fetch = vi.fn();

test('fetches data successfully', async () => {
  global.fetch.mockResolvedValue({
    json: async () => ({ name: 'John' })
  });

  const { result } = renderHook(() => useFetch('/api/user'));

  // Początkowo loading
  expect(result.current.loading).toBe(true);
  expect(result.current.data).toBe(null);

  // Czekaj na dane
  await waitFor(() => {
    expect(result.current.loading).toBe(false);
  });

  expect(result.current.data).toEqual({ name: 'John' });
  expect(result.current.error).toBe(null);
});

test('handles fetch error', async () => {
  global.fetch.mockRejectedValue(new Error('Network error'));

  const { result } = renderHook(() => useFetch('/api/user'));

  await waitFor(() => {
    expect(result.current.loading).toBe(false);
  });

  expect(result.current.error).toBe('Network error');
  expect(result.current.data).toBe(null);
});
💪 Ćwiczenie 5: Test custom hook useToggle

Hook useToggle:

function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);
  const toggle = () => setValue(v => !v);
  const setTrue = () => setValue(true);
  const setFalse = () => setValue(false);
  return { value, toggle, setTrue, setFalse };
}

Napisz testy dla:

  1. Wartość początkowa to false
  2. Wartość początkowa to true gdy przekażesz true
  3. toggle() zmienia false → true
  4. toggle() zmienia true → false
  5. setTrue() ustawia na true
  6. setFalse() ustawia na false
Mocking

Mock functions

🔍 Po co mockować funkcje?

Mock = fałszywa funkcja która udaje prawdziwą. Używamy gdy:

  • Chcemy sprawdzić czy funkcja została wywołana
  • Chcemy sprawdzić z jakimi argumentami
  • Chcemy kontrolować co funkcja zwraca
  • Prawdziwa funkcja jest "droga" (API call, zapis do bazy)
import { vi } from 'vitest';

test('mocking functions', () => {
  const mockFn = vi.fn();

  mockFn('arg1', 'arg2');
  mockFn('arg3');

  expect(mockFn).toHaveBeenCalledTimes(2);
  expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
  expect(mockFn).toHaveBeenLastCalledWith('arg3');
});

test('mock return value', () => {
  const mockFn = vi.fn(() => 'mocked value');
  expect(mockFn()).toBe('mocked value');

  // Lub
  mockFn.mockReturnValue('another value');
  expect(mockFn()).toBe('another value');

  // Raz
  mockFn.mockReturnValueOnce('once').mockReturnValue('always');
  expect(mockFn()).toBe('once');
  expect(mockFn()).toBe('always');
});
🌟 Przykład: Mock callback w komponencie
function SearchBar({ onSearch }) {
  const [query, setQuery] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    onSearch(query);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <button type="submit">Search</button>
    </form>
  );
}

// Test
test('calls onSearch with query', async () => {
  const mockOnSearch = vi.fn();
  const user = userEvent.setup();

  render(<SearchBar onSearch={mockOnSearch} />);

  await user.type(screen.getByRole('textbox'), 'React hooks');
  await user.click(screen.getByRole('button', { name: 'Search' }));

  expect(mockOnSearch).toHaveBeenCalledWith('React hooks');
  expect(mockOnSearch).toHaveBeenCalledTimes(1);
});

Mock modules

🔍 Mockowanie całych modułów

Czasami musisz zamockować cały plik (np. api.ts z funkcjami API). vi.mock() robi to automatycznie!

// api.ts
export async function fetchUsers() {
  const response = await fetch('/api/users');
  return response.json();
}
// Component.test.tsx
import { vi } from 'vitest';
import * as api from './api';

vi.mock('./api', () => ({
  fetchUsers: vi.fn()
}));

test('uses mocked API', async () => {
  const mockUsers = [{ id: 1, name: 'John' }];
  vi.mocked(api.fetchUsers).mockResolvedValue(mockUsers);

  // Test komponentu używającego fetchUsers
});

Mock React Router

🔍 Testowanie z React Router

Komponenty używające useNavigate, useParams potrzebują Router'a. Używamy MemoryRouter do testów!

import { vi } from 'vitest';
import { MemoryRouter } from 'react-router-dom';

vi.mock('react-router-dom', async () => {
  const actual = await vi.importActual('react-router-dom');
  return {
    ...actual,
    useNavigate: () => vi.fn(),
    useParams: () => ({ id: '123' })
  };
});

// Lub użyj MemoryRouter w testach
test('navigation', () => {
  render(
    <MemoryRouter initialEntries={['/users/123']}>
      <UserProfile />
    </MemoryRouter>
  );
});
🌟 Przykład: Test z MemoryRouter
function UserProfile() {
  const { id } = useParams();
  const navigate = useNavigate();

  return (
    <div>
      <h1>User {id}</h1>
      <button onClick={() => navigate('/users')}>Back</button>
    </div>
  );
}

// Test
test('displays user ID from params', () => {
  render(
    <MemoryRouter initialEntries={['/users/42']}>
      <Routes>
        <Route path="/users/:id" element={<UserProfile />} />
      </Routes>
    </MemoryRouter>
  );

  expect(screen.getByText('User 42')).toBeInTheDocument();
});
Testowanie Context

🔍 Jak testować Context?

Context musi mieć Provider! W testach:

  1. Owijamy testowy komponent w Provider
  2. Testujemy czy Context dostarcza poprawne wartości
  3. Testujemy czy funkcje z Context działają
// ThemeContext.tsx
const ThemeContext = createContext<{ theme: string; toggleTheme: () => void } | undefined>(undefined);

export function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useState('light');
  const toggleTheme = () => setTheme(t => t === 'light' ? 'dark' : 'light');

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) throw new Error('useTheme must be used within ThemeProvider');
  return context;
}
// ThemeContext.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ThemeProvider, useTheme } from './ThemeContext';

function TestComponent() {
  const { theme, toggleTheme } = useTheme();
  return (
    <div>
      <p>Current theme: {theme}</p>
      <button onClick={toggleTheme}>Toggle</button>
    </div>
  );
}

describe('ThemeContext', () => {
  test('provides default theme', () => {
    render(
      <ThemeProvider>
        <TestComponent />
      </ThemeProvider>
    );

    expect(screen.getByText(/current theme: light/i)).toBeInTheDocument();
  });

  test('toggles theme', async () => {
    const user = userEvent.setup();

    render(
      <ThemeProvider>
        <TestComponent />
      </ThemeProvider>
    );

    expect(screen.getByText(/light/i)).toBeInTheDocument();

    await user.click(screen.getByRole('button'));

    expect(screen.getByText(/dark/i)).toBeInTheDocument();
  });
});
🌟 Przykład: Test Auth Context
function AuthProvider({ children }) {
  const [user, setUser] = useState(null);

  const login = async (email, password) => {
    const response = await fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify({ email, password })
    });
    const data = await response.json();
    setUser(data.user);
  };

  const logout = () => setUser(null);

  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

// Test
test('login sets user', async () => {
  global.fetch = vi.fn().mockResolvedValue({
    json: async () => ({ user: { id: 1, name: 'John' } })
  });

  function TestComponent() {
    const { user, login } = useAuth();
    return (
      <div>
        {user ? <p>Logged in as {user.name}</p> : <p>Not logged in</p>}
        <button onClick={() => login('test@test.com', 'pass')}>Login</button>
      </div>
    );
  }

  const user = userEvent.setup();
  render(
    <AuthProvider>
      <TestComponent />
    </AuthProvider>
  );

  expect(screen.getByText('Not logged in')).toBeInTheDocument();

  await user.click(screen.getByRole('button', { name: 'Login' }));

  await waitFor(() => {
    expect(screen.getByText('Logged in as John')).toBeInTheDocument();
  });
});
Test helpers i utilities

Custom render z providers

🔍 Po co custom render?

Gdy każdy test potrzebuje tych samych providerów (Theme, Auth, Router), męczące jest owijanie za każdym razem:

// ❌ Powtarzalny kod w każdym teście
render(
  <ThemeProvider>
    <AuthProvider>
      <RouterProvider>
        <MyComponent />
      </RouterProvider>
    </AuthProvider>
  </ThemeProvider>
);

Rozwiązanie: Stwórz własny renderWithProviders!

// test/utils.tsx
import { render, RenderOptions } from '@testing-library/react';
import { ReactElement } from 'react';
import { ThemeProvider } from '../contexts/ThemeContext';
import { AuthProvider } from '../contexts/AuthContext';

function AllTheProviders({ children }: { children: React.ReactNode }) {
  return (
    <ThemeProvider>
      <AuthProvider>
        {children}
      </AuthProvider>
    </ThemeProvider>
  );
}

export function renderWithProviders(
  ui: ReactElement,
  options?: Omit<RenderOptions, 'wrapper'>
) {
  return render(ui, { wrapper: AllTheProviders, ...options });
}

export * from '@testing-library/react';

Użycie:

import { renderWithProviders } from './test/utils';

test('component with providers', () => {
  renderWithProviders(<MyComponent />);
  // ...
});
🌟 Przykład: Custom render z opcjami
// Możesz dodać opcje do custom render!
export function renderWithProviders(
  ui: ReactElement,
  {
    initialTheme = 'light',
    initialUser = null,
    ...options
  } = {}
) {
  function Wrapper({ children }) {
    return (
      <ThemeProvider initialTheme={initialTheme}>
        <AuthProvider initialUser={initialUser}>
          {children}
        </AuthProvider>
      </ThemeProvider>
    );
  }

  return render(ui, { wrapper: Wrapper, ...options });
}

// Użycie z opcjami
test('component with dark theme', () => {
  renderWithProviders(<MyComponent />, {
    initialTheme: 'dark'
  });
});
Coverage (pokrycie testami)

🔍 Co to jest code coverage?

Coverage = procent kodu który jest "dotknięty" przez testy. Pokazuje:

  • % Stmts - procent linii kodu
  • % Branch - procent if/else branchy
  • % Funcs - procent funkcji
  • % Lines - procent linii

80%+ = dobry coverage! Nie musisz mieć 100% - to często niemożliwe i niepraktyczne.

npm run test:coverage
----------------------|---------|----------|---------|---------|
File                  | % Stmts | % Branch | % Funcs | % Lines |
----------------------|---------|----------|---------|---------|
All files             |   85.71 |    83.33 |   88.88 |   85.71 |
 Button.tsx           |     100 |      100 |     100 |     100 |
 LoginForm.tsx        |   90.91 |       75 |     100 |   90.91 |
 UserProfile.tsx      |      75 |       80 |      75 |      75 |
----------------------|---------|----------|---------|---------|

Cel: 80%+ coverage, ale nie 100% za wszelką cenę!

🔍 Co NIE testować (zmarnowany czas):
  • Biblioteki zewnętrzne (React, Zustand - mają swoje testy)
  • Proste gettery/settery
  • Autogenerowany kod
  • Kod który "nigdy nie może się wykonać" (obsługa niemożliwych błędów)

Lepiej: 80% coverage dobrych testów niż 100% z bezsensownymi testami!

Best Practices

1. Testuj ZACHOWANIE, nie IMPLEMENTACJĘ

🔍 Testuj ZACHOWANIE, nie IMPLEMENTACJĘ (Test behavior, not implementation)

Zachowanie = co użytkownik widzi i robi
Implementacja = jak to działa w środku (useState, useEffect)

Dlaczego? Implementacja może się zmienić (useState → Zustand), ale zachowanie pozostaje!

// ❌ Źle - testuje implementację
test('bad test', () => {
  const { result } = renderHook(() => useState(0));
  expect(result.current[0]).toBe(0); // Testuje useState!
});

// ✅ Dobrze - testuje zachowanie
test('good test', () => {
  render(<Counter />);
  expect(screen.getByText('Count: 0')).toBeInTheDocument();
  
  // Testuje co użytkownik widzi i robi
});

2. Testuj dostępność

test('button is accessible', () => {
  render(<Button label="Submit" onClick={() => {}} />);

  const button = screen.getByRole('button', { name: /submit/i });
  expect(button).toBeInTheDocument();
  expect(button).toBeEnabled();
});

3. Organizuj testy

describe('TodoList', () => {
  describe('rendering', () => {
    test('displays empty state when no todos', () => {});
    test('displays todos when present', () => {});
  });

  describe('interactions', () => {
    test('adds new todo', () => {});
    test('toggles todo', () => {});
    test('deletes todo', () => {});
  });

  describe('filtering', () => {
    test('shows all todos by default', () => {});
    test('filters active todos', () => {});
    test('filters completed todos', () => {});
  });
});

4. Używaj data-testid w ostateczności

🔍 Kiedy używać data-testid?

Rzadko. data-testid powinien być używany tylko wtedy, gdy nie da się sensownie odwołać do elementu w sposób zbliżony do tego, jak robi to użytkownik.

Testy powinny odzwierciedlać realne użycie aplikacji — klikanie przycisków, wypełnianie pól, czytanie tekstów. Dlatego zawsze należy w pierwszej kolejności korzystać z query opartych o semantykę, dostępność i widoczny interfejs.

Zalecana kolejność:

  1. getByRole – najlepsze rozwiązanie, testuje semantykę HTML i zgodność z accessibility (np. przyciski, linki, checkboxy).
  2. getByLabelText – idealne dla formularzy, odzwierciedla sposób, w jaki użytkownik identyfikuje pola.
  3. getByText – dobre, gdy element jest rozpoznawalny po treści, którą widzi użytkownik.
  4. ...inne queries zależne od kontekstu...
  5. getByTestIdostateczność, gdy element nie ma sensownej semantyki lub nie jest dostępny z perspektywy użytkownika.

Nadużywanie data-testid prowadzi do testów zależnych od implementacji, które mogą przechodzić mimo realnych problemów w interfejsie użytkownika.

// ❌ Nadużycie
<button data-testid="submit-button">Submit</button>
screen.getByTestId('submit-button');

// ✅ Lepiej - semantic queries
<button>Submit</button>
screen.getByRole('button', { name: /submit/i });

// ✅ OK dla złożonych przypadków
<div data-testid="complex-widget">...</div>

5. Testy powinny być proste

// ❌ Test robi za dużo
test('complete user flow', async () => {
  // 50 linii testu testującego wszystko naraz
});

// ✅ Małe, fokusowane testy
test('user can login', async () => {
  // Tylko login
});

test('logged in user can create post', async () => {
  // Tylko tworzenie posta
});
💪 Ćwiczenie 6: Oceń testy

Które testy są dobre, a które złe?

// Test A
test('counter', () => {
  const { result } = renderHook(() => useState(0));
  expect(typeof result.current[1]).toBe('function');
});

// Test B
test('counter displays initial value', () => {
  render(<Counter />);
  expect(screen.getByText('Count: 0')).toBeInTheDocument();
});

// Test C
test('complete flow', async () => {
  render(<App />);
  await user.click(screen.getByTestId('login-btn'));
  await user.type(screen.getByTestId('email'), 'test@test.com');
  await user.type(screen.getByTestId('password'), 'pass');
  await user.click(screen.getByTestId('submit'));
  await user.click(screen.getByTestId('dashboard'));
  // ... 20 linii więcej
});

Odpowiedzi: A = zły (testuje implementację), B = dobry (testuje zachowanie), C = zły (za długi, za dużo testid)

Praktyczny przykład: TodoList

// TodoList.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, test, expect } from 'vitest';
import TodoList from './TodoList';

describe('TodoList', () => {
  test('adds new todo', async () => {
    const user = userEvent.setup();
    render(<TodoList />);

    const input = screen.getByPlaceholderText(/add todo/i);
    await user.type(input, 'Buy milk');
    await user.keyboard('{Enter}');

    expect(screen.getByText('Buy milk')).toBeInTheDocument();
    expect(input).toHaveValue(''); // Input wyczyszczony
  });

  test('toggles todo completion', async () => {
    const user = userEvent.setup();
    render(<TodoList />);

    // Dodaj todo
    await user.type(screen.getByPlaceholderText(/add todo/i), 'Buy milk{Enter}');

    // Toggle
    const checkbox = screen.getByRole('checkbox');
    expect(checkbox).not.toBeChecked();

    await user.click(checkbox);
    expect(checkbox).toBeChecked();

    await user.click(checkbox);
    expect(checkbox).not.toBeChecked();
  });

  test('deletes todo', async () => {
    const user = userEvent.setup();
    render(<TodoList />);

    // Dodaj todo
    await user.type(screen.getByPlaceholderText(/add todo/i), 'Buy milk{Enter}');

    expect(screen.getByText('Buy milk')).toBeInTheDocument();

    // Usuń
    await user.click(screen.getByRole('button', { name: /delete/i }));

    expect(screen.queryByText('Buy milk')).not.toBeInTheDocument();
  });

  test('filters todos', async () => {
    const user = userEvent.setup();
    render(<TodoList />);

    // Dodaj todos
    await user.type(screen.getByPlaceholderText(/add todo/i), 'Task 1{Enter}');
    await user.type(screen.getByPlaceholderText(/add todo/i), 'Task 2{Enter}');

    // Toggle pierwszy
    const checkboxes = screen.getAllByRole('checkbox');
    await user.click(checkboxes[0]);

    // Filtruj active
    await user.click(screen.getByRole('button', { name: /active/i }));
    expect(screen.getByText('Task 2')).toBeInTheDocument();
    expect(screen.queryByText('Task 1')).not.toBeInTheDocument();

    // Filtruj completed
    await user.click(screen.getByRole('button', { name: /completed/i }));
    expect(screen.getByText('Task 1')).toBeInTheDocument();
    expect(screen.queryByText('Task 2')).not.toBeInTheDocument();
  });
});
🌟 Analiza testów TodoList:
  • Fokusowane - każdy test sprawdza jedną funkcję
  • Czytelne - nazwy testów mówią co testują
  • Realistyczne - używają userEvent (jak prawdziwy użytkownik)
  • Kompletne - pokrywają wszystkie główne funkcje (CRUD + filtry)
  • Niezależne - każdy test zaczyna od zera (render)
Podsumowanie

To był techniczny, ale bardzo ważny wpis! Nauczyliśmy się:

  • Rodzaje testów – unit, integration, E2E
  • Setup – Vitest + Testing Library
  • Podstawy – matchers, queries, assertions
  • Testowanie komponentów – render, queries, user interactions
  • User events – userEvent vs fireEvent
  • Formularze – testowanie inputs, submit
  • Async code – waitFor, findBy, mocking fetch
  • Custom hooks – renderHook, act
  • Mocking – functions, modules, Router
  • Context – testowanie z providers
  • Test utilities – custom render
  • Coverage – jak mierzyć pokrycie
  • Best Practices – 5 złotych zasad
  • TodoList – kompletny przykład

Testy to inwestycja – na początku zabierają czas, ale zwracają się wielokrotnie. Dają pewność, przyspieszają development, umożliwiają refactoring. Po tym wpisie jesteś gotowy pisać testy jak profesjonalista!

W kolejnym wpisie poznamy Error Boundaries i obsługę błędów – jak poprawnie obsługiwać błędy w React, logowanie, monitrowanie!

Zadanie dla Ciebie

Napisz testy dla swojej aplikacji:

  1. Przetestuj główne komponenty (min 3)
  2. Przetestuj custom hook
  3. Przetestuj formularz z walidacją
  4. Przetestuj async operację
  5. Osiągnij min 80% coverage
  6. (Bonus) Dodaj CI/CD z automatycznymi testami
🎯 BONUS: Wielki projekt końcowy - Test Suite dla Shopping Cart

Stwórz kompletny zestaw testów dla koszyka zakupowego:

Komponenty do przetestowania:

  • ProductCard - karta produktu z przyciskiem "Dodaj do koszyka"
  • Cart - lista produktów w koszyku z ilościami
  • CartSummary - podsumowanie (suma, liczba produktów)
  • CheckoutForm - formularz płatności

Co przetestować:

  1. ProductCard:
    • Wyświetla nazwę, cenę, zdjęcie
    • Przycisk wywołuje callback z produktem
    • Disabled gdy out of stock
  2. Cart:
    • Pusty koszyk pokazuje "Koszyk pusty"
    • Wyświetla produkty z ilościami
    • Można zwiększyć/zmniejszyć ilość
    • Można usunąć produkt
  3. CartSummary:
    • Oblicza poprawną sumę
    • Oblicza liczbę produktów
    • Pokazuje kod rabatowy jeśli zastosowany
  4. CheckoutForm:
    • Waliduje dane karty kredytowej
    • Waliduje adres
    • Pokazuje błędy walidacji
    • Submit wysyła poprawne dane
  5. Integration test:
    • Dodaj 3 produkty → sprawdź sumę
    • Zmień ilości → sprawdź nową sumę
    • Usuń produkt → sprawdź aktualizację
    • Wypełnij checkout → sprawdź submit

Wymagania techniczne:

  • ✅ Minimum 20 testów (po ~4 na komponent)
  • ✅ Użyj describe do organizacji
  • ✅ Mock fetch/API calls
  • ✅ Użyj userEvent (nie fireEvent)
  • ✅ Test async operations z waitFor
  • ✅ Custom render z CartProvider
  • ✅ Osiągnij 90%+ coverage
  • ✅ Dodaj 1 E2E test (Cypress/Playwright)

To profesjonalny test suite! Po ukończeniu tego projektu będziesz prawdziwym ekspertem od testowania React! 🧪✨