Spaces:
Sleeping
Sleeping
Testing Guide
This document describes the testing approach for the Paper Trail Web application.
Testing Stack
| Tool | Purpose |
|---|---|
| Vitest | Test runner and assertion library |
| React Testing Library | Component testing utilities |
| jsdom | DOM simulation for Node.js |
| @testing-library/user-event | User interaction simulation |
| @testing-library/jest-dom | Custom DOM matchers |
Running Tests
# Run tests in watch mode (development)
pnpm test
# Run tests once (CI)
pnpm test:run
# Run tests with UI
pnpm test:ui
Test File Organization
Tests are co-located with the code they test:
src/
βββ components/
β βββ Header.tsx
β βββ Header.test.tsx # Co-located test
βββ utils/
β βββ formatters.ts
β βββ formatters.test.ts # Co-located test
βββ test/
βββ setup.ts # Global test setup
Writing Tests
Component Tests
Use React Testing Library to test components:
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect } from 'vitest';
import { MyComponent } from './MyComponent';
describe('MyComponent', () => {
it('renders correctly', () => {
render(<MyComponent title="Hello" />);
expect(screen.getByText('Hello')).toBeInTheDocument();
});
it('handles user interaction', async () => {
const user = userEvent.setup();
render(<MyComponent onSubmit={vi.fn()} />);
await user.click(screen.getByRole('button', { name: /submit/i }));
expect(/* assertion */).toBe(/* expected */);
});
});
Testing with React Query
Wrap components that use React Query:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { render } from '@testing-library/react';
function renderWithQuery(ui: React.ReactElement) {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
return render(
<QueryClientProvider client={queryClient}>
{ui}
</QueryClientProvider>
);
}
Testing with React Router
Wrap components that use routing:
import { MemoryRouter } from 'react-router-dom';
import { render } from '@testing-library/react';
function renderWithRouter(ui: React.ReactElement, { route = '/' } = {}) {
return render(
<MemoryRouter initialEntries={[route]}>
{ui}
</MemoryRouter>
);
}
Mocking API Calls
Mock the API service for isolated testing:
import { vi } from 'vitest';
import { api } from '../services/api';
vi.mock('../services/api');
describe('ComponentWithAPI', () => {
it('displays data from API', async () => {
vi.mocked(api.searchPoliticians).mockResolvedValue([
{ id: '1', name: 'Test Politician' },
]);
render(<ComponentWithAPI />);
await waitFor(() => {
expect(screen.getByText('Test Politician')).toBeInTheDocument();
});
});
});
Testing Patterns
Query by Role (Preferred)
Prefer accessible queries that reflect how users interact with the UI:
// Good - queries by accessible role
screen.getByRole('button', { name: /submit/i });
screen.getByRole('textbox', { name: /search/i });
screen.getByRole('heading', { level: 1 });
// Avoid - queries by implementation details
screen.getByTestId('submit-button');
screen.getByClassName('btn-primary');
Async Operations
Use waitFor or findBy queries for async operations:
// Option 1: waitFor
await waitFor(() => {
expect(screen.getByText('Loaded')).toBeInTheDocument();
});
// Option 2: findBy (combines getBy + waitFor)
const element = await screen.findByText('Loaded');
expect(element).toBeInTheDocument();
User Events
Use userEvent over fireEvent for realistic interactions:
const user = userEvent.setup();
// Typing
await user.type(input, 'hello');
// Clicking
await user.click(button);
// Keyboard navigation
await user.keyboard('{Enter}');
await user.tab();
Test Configuration
Vitest Config (vitest.config.ts)
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./src/test/setup.ts'],
css: true,
},
});
Test Setup (src/test/setup.ts)
import '@testing-library/jest-dom/vitest';
import { cleanup } from '@testing-library/react';
import { afterEach } from 'vitest';
// Cleanup after each test
afterEach(() => {
cleanup();
});
What to Test
Component Testing Priorities
- User interactions: Clicks, typing, form submissions
- Conditional rendering: Show/hide logic based on state
- Data display: Correct rendering of fetched data
- Error states: Error messages and fallbacks
- Loading states: Loading indicators
- Accessibility: Keyboard navigation, ARIA attributes
Skip Testing
- Implementation details (internal state, private methods)
- Third-party library behavior
- Styling/CSS (use visual regression tests if needed)
Example Tests
Testing a Search Component
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { vi, describe, it, expect, beforeEach } from 'vitest';
import { SearchInput } from './SearchInput';
describe('SearchInput', () => {
const mockOnSearch = vi.fn();
beforeEach(() => {
mockOnSearch.mockClear();
});
it('calls onSearch when user types and submits', async () => {
const user = userEvent.setup();
render(<SearchInput onSearch={mockOnSearch} />);
const input = screen.getByRole('textbox', { name: /search/i });
await user.type(input, 'test query');
await user.keyboard('{Enter}');
expect(mockOnSearch).toHaveBeenCalledWith('test query');
});
it('debounces search calls', async () => {
const user = userEvent.setup();
render(<SearchInput onSearch={mockOnSearch} debounceMs={100} />);
const input = screen.getByRole('textbox');
await user.type(input, 'abc');
// Should not call immediately
expect(mockOnSearch).not.toHaveBeenCalled();
// Wait for debounce
await waitFor(() => {
expect(mockOnSearch).toHaveBeenCalledWith('abc');
});
});
});
Testing Error States
describe('PoliticianProfile', () => {
it('displays error message when API fails', async () => {
vi.mocked(api.getPolitician).mockRejectedValue(new Error('Not found'));
renderWithQuery(<PoliticianProfile id="invalid" />);
await waitFor(() => {
expect(screen.getByText(/error/i)).toBeInTheDocument();
});
});
});
CI Integration
Tests run automatically on every PR and push to main:
# .github/workflows/ci.yml
- name: Run tests
run: pnpm run test:run
Debugging Tests
Vitest UI
Run pnpm test:ui for an interactive test dashboard.
Debug Output
Use screen.debug() to print the current DOM:
it('debugging example', () => {
render(<MyComponent />);
screen.debug(); // Prints current DOM to console
});
Playground
Use Testing Library's screen.logTestingPlaygroundURL() to generate a URL for the Testing Playground tool.