1. The Testing Philosophy
In modern React, we test behavior, not implementation details. We don't care if a component has a state variable named `isOpen`. We care if the user can see the modal when they click the button.
Key Tools
- Vitest: The runner (fast, compatible with Vite). Replaces Jest.
- React Testing Library (RTL): The utility to render components and query the DOM like a user (`getByText`, `findByRole`).
- MSW (Mock Service Worker): The industry standard for mocking network requests. It intercepts calls at the network layer.
Setup Test Infrastructure
As a developer, I need a robust testing environment to ensure my application is stable before deployment.
- Install
vitest,jsdom, and@testing-library/react. - Configure
vite.config.tsto use the `jsdom` environment. - Add a
testscript to `package.json`.
npm install -D vitest jsdom @testing-library/react @testing-library/jest-dom
vite.config.ts:
///
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
setupFiles: './src/test/setup.ts',
globals: true // optional, allows using describe/it without import
}
})
Unit & Integration Tests
As a QA Engineer, I want to verify that individual components render correctly and respond to user interactions.
- UserCard: Verify it renders the `name` and `role` props correctly.
- Counter: Verify clicking "Increment" updates the count text.
- Use
screen.getByRoleorscreen.getByTextfor queries (accessible queries).
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import UserCard from './UserCard';
describe('UserCard', () => {
it('renders user information correctly', () => {
render(<UserCard name="Alice" role="Admin" />);
// Check if text exists in the document
expect(screen.getByText('Alice')).toBeInTheDocument();
expect(screen.getByText(/admin/i)).toBeInTheDocument();
});
});
import { render, screen, fireEvent } from '@testing-library/react';
import Counter from './Counter';
it('increments count on click', () => {
render(<Counter />);
const button = screen.getByRole('button', { name: /increment/i });
// Initial state
expect(screen.getByText('Count: 0')).toBeInTheDocument();
// Interaction
fireEvent.click(button);
// Assert new state
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
Async & Mocking (Advanced)
As a developer, I want to test data fetching components without relying on the real backend API.
- Mock the
fetchglobal or use MSW. - Verify the "Loading..." state appears initially.
- Verify the data appears after the async operation completes using
await screen.findBy....
import { render, screen, waitFor } from '@testing-library/react';
import { vi } from 'vitest';
import UserList from './UserList';
// Simple fetch mock
global.fetch = vi.fn();
function createFetchResponse(data) {
return { json: () => new Promise((resolve) => resolve(data)) };
}
it('renders users after fetching', async () => {
// Setup mock response
(fetch as any).mockResolvedValue(createFetchResponse([
{ id: 1, name: 'John Doe' }
]));
render(<UserList />);
// 1. Check Loading State
expect(screen.getByText(/loading/i)).toBeInTheDocument();
// 2. Check Async Result (findBy waits for appearance)
const userItem = await screen.findByText('John Doe');
expect(userItem).toBeInTheDocument();
});
User Story: As a developer, I want to test my component's data fetching logic without hitting the real API.
Acceptance Criteria:
- Use MSW (Mock Service Worker) to intercept requests.
- Test the "Loading" state.
- Test the "Success" state (data renders).
- Test the "Error" state (error message renders).
2. Types of Testing
Unit Tests: Testing individual functions or components in isolation (e.g., `utils/formatDate.ts`).
Integration Tests: Testing how multiple units work together (e.g., `UserProfile` component fetching data and rendering `UserCard`).
E2E (End-to-End): Testing the full flow in a real browser (Playwright/Cypress). We will focus on Unit/Integration here.
🐛 Bug Hunt: The "Not Wrapped in Act" Warning
Scenario: You run a test and see a console error: Warning: An update to Component inside a test was not wrapped in act(...).
Why? Your component updated state (like after a fetch resolves) after your test finished or while your test was doing something else. React wants you to signal that an update is happening.
Fix: Usually, you don't need to use `act()` manually. Instead, use async queries like findByText which automatically wait for updates.
// ❌ Bad: Asserting immediately
// expect(screen.getByText('Data')).toBeInTheDocument(); // Fails, data isn't there yet
// ✅ Good: Waiting for appearance
// await screen.findByText('Data'); // Waits up to 1000ms (default)