Testing React applications is divided into three levels: Unit (fast), Integration (checking connections), E2E (as a user).
Simple analogy: Imagine you are testing a coffee machine.
1. Unit tests (Vitest/Jest + React Testing Library):
1import { render, screen } from '@testing-library/react';2import userEvent from '@testing-library/user-event';3import Counter from './Counter';45test('counter increases on click', async () => {6 render(<Counter />);7 const button = screen.getByRole('button', { name: /increase/i });8 await userEvent.click(button);9 expect(screen.getByText('1')).toBeInTheDocument();10});
Principle: Test behavior (what the user sees), not implementation (do not check state directly).
2. Integration tests (RTL + Mock Service Worker): Replace API and check whole scenarios:
1test('loading user list', async () => {2 render(<UserList />);3 expect(await screen.findByText('Alice')).toBeInTheDocument();4 expect(await screen.findByText('Bob')).toBeInTheDocument();5});
3. E2E tests (Playwright / Cypress): As a user in a browser:
1test('user can login', async ({ page }) => {2 await page.goto('/login');3 await page.fill('[name="email"]', 'test@test.com');4 await page.fill('[name="password"]', 'password123');5 await page.click('button[type="submit"]');6 await expect(page.locator('h1')).toHaveText('Welcome!');7});
What is important: