Testing Guide
Complete guide to testing the AI Store application.
Testing Strategy
Test Types
- Unit Tests: Test individual functions/utilities
- Component Tests: Test React components
- Integration Tests: Test feature flows
- E2E Tests: Test complete user journeys
Setup
Install Dependencies
npm install --save-dev @testing-library/react @testing-library/jest-dom jest jest-environment-jsdom
Jest Configuration
Create jest.config.js:
const nextJest = require('next/jest')
const createJestConfig = nextJest({
dir: './',
})
const customJestConfig = {
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
testEnvironment: 'jest-environment-jsdom',
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/$1',
},
}
module.exports = createJestConfig(customJestConfig)
Jest Setup
Create jest.setup.js:
import '@testing-library/jest-dom'
Unit Testing
Testing Utilities
import { formatNumber, formatCurrency } from '@/lib/utils';
describe('formatNumber', () => {
it('formats numbers with commas', () => {
expect(formatNumber(1000)).toBe('1,000');
expect(formatNumber(1000000)).toBe('1,000,000');
});
});
describe('formatCurrency', () => {
it('formats currency', () => {
expect(formatCurrency(99.99, 'USD')).toBe('$99.99');
});
});
Testing Hooks
import { renderHook, act } from '@testing-library/react';
import { useForm } from '@/hooks/useForm';
describe('useForm', () => {
it('handles form submission', async () => {
const onSubmit = jest.fn();
const { result } = renderHook(() =>
useForm({
initialValues: { email: '' },
onSubmit,
})
);
act(() => {
result.current.handleChange({
target: { name: 'email', value: 'test@example.com' },
});
});
await act(async () => {
await result.current.handleSubmit({
preventDefault: () => {},
} as any);
});
expect(onSubmit).toHaveBeenCalledWith({ email: 'test@example.com' });
});
});
Component Testing
Basic Component Test
import { render, screen } from '@testing-library/react';
import Button from '@/components/Button';
describe('Button', () => {
it('renders button text', () => {
render(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('handles click events', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);
screen.getByText('Click me').click();
expect(handleClick).toHaveBeenCalledTimes(1);
});
});
Testing with Providers
import { render } from '@testing-library/react';
import { ToastProvider } from '@/components/Toast';
const renderWithProviders = (ui: React.ReactElement) => {
return render(
<ToastProvider>
{ui}
</ToastProvider>
);
};
describe('MyComponent', () => {
it('renders with providers', () => {
renderWithProviders(<MyComponent />);
});
});
Testing Async Components
import { render, screen, waitFor } from '@testing-library/react';
import DataComponent from '@/components/DataComponent';
describe('DataComponent', () => {
it('loads and displays data', async () => {
render(<DataComponent />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText('Data loaded')).toBeInTheDocument();
});
});
});
Integration Testing
Testing Form Flows
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import MyForm from '@/components/MyForm';
describe('MyForm Integration', () => {
it('submits form with valid data', async () => {
const onSubmit = jest.fn();
render(<MyForm onSubmit={onSubmit} />);
fireEvent.change(screen.getByLabelText('Email'), {
target: { value: 'test@example.com' },
});
fireEvent.change(screen.getByLabelText('Password'), {
target: { value: 'password123' },
});
fireEvent.click(screen.getByRole('button', { name: 'Submit' }));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123',
});
});
});
});
Testing API Integration
import { render, screen, waitFor } from '@testing-library/react';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import DataComponent from '@/components/DataComponent';
const server = setupServer(
rest.get('/api/data', (req, res, ctx) => {
return res(ctx.json({ data: 'test' }));
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
describe('DataComponent API Integration', () => {
it('fetches and displays data', async () => {
render(<DataComponent />);
await waitFor(() => {
expect(screen.getByText('test')).toBeInTheDocument();
});
});
});
Mocking
Mock Functions
import { jest } from '@jest/globals';
const mockFunction = jest.fn();
mockFunction.mockReturnValue('value');
mockFunction.mockResolvedValue('async value');
Mock Modules
jest.mock('@/lib/api', () => ({
fetchData: jest.fn().mockResolvedValue({ data: 'test' }),
}));
Mock Next.js
jest.mock('next/navigation', () => ({
useRouter: () => ({
push: jest.fn(),
replace: jest.fn(),
}),
usePathname: () => '/',
}));
Test Utilities
Custom Render
import { render } from '@testing-library/react';
import { customRender } from '@/lib/test-utils';
// Use custom render with providers
const { container } = customRender(<MyComponent />);
Mock Data
import { generateMockApp } from '@/lib/test-utils';
const mockApp = generateMockApp({
title: 'Test App',
rating: 4.5,
});
Best Practices
1. Test Behavior, Not Implementation
// Good: Test behavior
expect(screen.getByText('Success')).toBeInTheDocument();
// Bad: Test implementation
expect(component.state.success).toBe(true);
2. Use Accessible Queries
// Prefer accessible queries
screen.getByRole('button', { name: 'Submit' });
screen.getByLabelText('Email');
screen.getByText('Welcome');
// Avoid non-accessible queries
screen.getByTestId('submit-button');
3. Keep Tests Isolated
// Each test should be independent
// Don't rely on test execution order
// Clean up after each test
4. Test Edge Cases
// Test empty states
// Test error states
// Test loading states
// Test boundary conditions
5. Use Descriptive Test Names
// Good
it('displays error message when email is invalid', () => {});
// Bad
it('test email', () => {});
Running Tests
Run All Tests
npm test
Run in Watch Mode
npm test -- --watch
Run with Coverage
npm test -- --coverage
Run Specific Test
npm test -- MyComponent.test.tsx
Coverage Goals
- Statements: > 80%
- Branches: > 75%
- Functions: > 80%
- Lines: > 80%
Test Organization
__tests__/
├── unit/
│ ├── utils.test.ts
│ └── hooks.test.ts
├── components/
│ ├── Button.test.tsx
│ └── Modal.test.tsx
└── integration/
├── form.test.tsx
└── api.test.tsx
Continuous Integration
GitHub Actions
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm ci
- run: npm test
- run: npm run test:coverage