Testing Setup & Guidelines
Overview
This project uses Vitest for unit testing with @testing-library/react for component testing. The setup includes MSW (Mock Service Worker) for API mocking and comprehensive test utilities.
Testing Stack
- Test Runner: Vitest 4.0.17
- Component Testing: @testing-library/react 16.3.1
- User Interactions: @testing-library/user-event 14.6.1
- API Mocking: MSW 2.12.7
- Coverage: @vitest/coverage-v8 4.0.17
- Environment: happy-dom 20.3.1
Project Structure
src/
โโโ test/
โ โโโ mocks/
โ โ โโโ server.ts # MSW server setup
โ โ โโโ handlers.ts # API request handlers
โ โโโ test-utils.tsx # Custom render function & providers
โ โโโ test-helpers.ts # Utility functions for tests
โโโ hooks/
โ โโโ use-mobile.tsx
โ โโโ use-mobile.test.tsx # Hook tests
โ โโโ ...
โโโ components/
โโโ common/
โโโ Tabs.tsx
โโโ Tabs.test.tsx # Component tests
โโโ ...
Running Tests
# Run tests in watch mode
bun run test
# Run tests once
bun run test:run
# Run tests with UI
bun run test:ui
# Generate coverage report
bun run test:coverage
Writing Tests
1. Hook Tests
import { describe, it, expect, vi } from 'vitest';
import { renderHook } from '@testing-library/react';
import { useMyHook } from '@/src/hooks/use-my-hook';
describe('useMyHook', () => {
it('should initialize with default value', () => {
const { result } = renderHook(() => useMyHook());
expect(result.current).toBe(defaultValue);
});
it('should update state on action', () => {
const { result } = renderHook(() => useMyHook());
act(() => {
result.current.doSomething();
});
expect(result.current.value).toBe(expectedValue);
});
});
2. Component Tests
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MyComponent } from '@/src/components/MyComponent';
describe('MyComponent', () => {
it('should render component', () => {
render(<MyComponent />);
expect(screen.getByText('Expected text')).toBeInTheDocument();
});
it('should handle user interactions', async () => {
const user = userEvent.setup();
const handleClick = vi.fn();
render(<MyComponent onClick={handleClick} />);
await user.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalled();
});
});
3. Service/Utility Tests
import { describe, it, expect, vi } from 'vitest';
import { myFunction } from '@/src/lib/my-utility';
vi.mock('@/src/services/apiClient', () => ({
apiClient: {
get: vi.fn(),
post: vi.fn(),
},
}));
describe('myFunction', () => {
it('should return expected result', () => {
const result = myFunction('input');
expect(result).toBe('expected output');
});
});
Mocking Strategies
API Requests (MSW)
The MSW server is automatically started for all tests. Add handlers to src/test/mocks/handlers.ts:
http.get('/api/endpoint', () => {
return HttpResponse.json({ data: 'mock data' }, { status: 200 });
}),
Module Mocking
vi.mock('@/src/services/apiClient', () => ({
apiClient: {
get: vi.fn(),
post: vi.fn(),
},
}));
Next.js Features
Pre-mocked in vitest.setup.ts:
- next/navigation (useRouter, usePathname, useSearchParams, useParams)
- next/image
- window.matchMedia
- localStorage & sessionStorage
Coverage Thresholds
Current targets (in vitest.config.ts):
- Lines: 60%
- Functions: 60%
- Branches: 60%
- Statements: 60%
To check coverage:
bun run test:coverage
Coverage reports are generated in coverage/ directory.
Test File Organization
- Place test files next to the source file with
.test.ts(x)or.spec.ts(x)suffix - Group related tests using
describe()blocks - Use meaningful test names that describe behavior
- Keep tests focused and isolated
Common Test Patterns
Testing Async Operations
it('should handle async operations', async () => {
const { result } = renderHook(() => useAsyncHook());
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(result.current.data).toBeDefined();
});
Testing State Changes
it('should update state', () => {
const { result } = renderHook(() => useState(false));
act(() => {
result.current[1](true);
});
expect(result.current[0]).toBe(true);
});
Testing Error Handling
it('should handle errors', async () => {
(apiClient.get as any).mockRejectedValue(new Error('Network error'));
const result = await myFunction();
expect(result).toBeNull(); // or expect specific error handling
});
Debugging Tests
Run with UI for visual debugging:
bun run test:ui
Use console.log() during tests - output will show in the test output.
For more complex debugging, check individual test file outputs.
Best Practices
- Isolation: Each test should be independent and not rely on others
- Clarity: Use descriptive test names (what, when, then pattern)
- Focus: Test one behavior per test
- Mocking: Mock external dependencies properly
- Coverage: Aim for good coverage but prioritize meaningful tests
- Maintainability: Keep tests simple and easy to understand
Next Steps
- [ ] Add tests for remaining hooks
- [ ] Add tests for form components
- [ ] Add tests for page components
- [ ] Set up CI/CD integration
- [ ] Increase coverage to 80%+
- [ ] Add E2E tests with Playwright or Cypress