Frontend Testing
This guide covers testing strategies and implementation for the Rhesis frontend application.
Testing Framework The frontend uses Jest as the test runner and React Testing Library for component testing, providing comprehensive unit testing capabilities.
Quick Start
Running Tests
Test Coverage Target coverage thresholds: 70% for branches, functions, lines, and statements
Testing Configuration
Jest Setup
The testing framework is configured in jest.config.js
:
Test Environment Setup
The test environment is configured in jest.setup.js
:
import '@testing-library/jest-dom'
// Set environment variables for tests
process.env.NEXT_PUBLIC_API_BASE_URL = 'http://localhost:8080/api/v1'
// Mock Next.js router
jest.mock('next/router', () => ({
useRouter() {
return {
route: '/',
pathname: '',
query: {},
asPath: '',
push: jest.fn(),
events: {
on: jest.fn(),
off: jest.fn(),
},
isFallback: false,
}
},
}))
// Mock browser APIs
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
})
global.ResizeObserver = jest.fn().mockImplementation(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn(),
}))
Writing Tests
Component Testing
Use React Testing Library to test component behavior and user interactions:
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import BaseDrawer from '../BaseDrawer';
import '@testing-library/jest-dom';
describe('BaseDrawer', () => {
const mockOnClose = jest.fn();
const mockOnSave = jest.fn();
afterEach(() => {
jest.clearAllMocks();
});
it('renders with open state', () => {
render(
<BaseDrawer open={true} onClose={mockOnClose} title="Test Drawer">
<div>Test Content</div>
</BaseDrawer>
);
expect(screen.getByText('Test Drawer')).toBeInTheDocument();
expect(screen.getByText('Test Content')).toBeInTheDocument();
});
it('calls onClose when Cancel button is clicked', async () => {
const user = userEvent.setup();
render(
<BaseDrawer open={true} onClose={mockOnClose} title="Test Drawer">
<div>Test Content</div>
</BaseDrawer>
);
await user.click(screen.getByRole('button', { name: /cancel/i }));
expect(mockOnClose).toHaveBeenCalledTimes(1);
});
it('disables buttons when loading is true', () => {
render(
<BaseDrawer
open={true}
onClose={mockOnClose}
onSave={mockOnSave}
title="Test Drawer"
loading={true}
>
<div>Test Content</div>
</BaseDrawer>
);
expect(screen.getByRole('button', { name: /cancel/i })).toBeDisabled();
expect(screen.getByRole('button', { name: /save changes/i })).toBeDisabled();
});
});
Hook Testing
Test custom hooks using @testing-library/react-hooks
:
import { renderHook, act } from '@testing-library/react'
import { useTasks } from '../useTasks'
import { TasksClient } from '@/utils/api-client/tasks-client'
// Mock the API client
jest.mock('@/utils/api-client/tasks-client')
const mockTasksClient = TasksClient as jest.Mocked<typeof TasksClient>
describe('useTasks', () => {
beforeEach(() => {
jest.clearAllMocks()
})
it('should fetch tasks on mount', async () => {
const mockTasks = [{ id: '1', name: 'Test Task' }]
mockTasksClient.getTasks.mockResolvedValue(mockTasks)
const { result } = renderHook(() => useTasks())
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 0))
})
expect(result.current.tasks).toEqual(mockTasks)
expect(result.current.isLoading).toBe(false)
})
it('should handle errors correctly', async () => {
const mockError = new Error('API Error')
mockTasksClient.getTasks.mockRejectedValue(mockError)
const { result } = renderHook(() => useTasks())
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 0))
})
expect(result.current.error).toBe('API Error')
expect(result.current.isLoading).toBe(false)
})
})
Utility Function Testing
Test utility functions with Jest:
import { formatDate } from '../date-utils'
describe('formatDate', () => {
it('formats ISO date string correctly', () => {
const isoDate = '2024-01-15T10:30:00Z'
const formatted = formatDate(isoDate)
expect(formatted).toBe('Jan 15, 2024')
})
it('handles invalid date gracefully', () => {
const invalidDate = 'invalid-date'
const formatted = formatDate(invalidDate)
expect(formatted).toBe('Invalid Date')
})
it('formats with custom locale', () => {
const isoDate = '2024-01-15T10:30:00Z'
const formatted = formatDate(isoDate, 'en-GB')
expect(formatted).toBe('15 Jan 2024')
})
})
Test Utilities
Custom Test Utils
Use custom test utilities for consistent test setup:
// src/__mocks__/test-utils.tsx
import { render, RenderOptions } from '@testing-library/react';
import { ReactElement } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
// Mock QueryClient for testing
const createTestQueryClient = () => new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
export const renderWithProviders = (
ui: ReactElement,
options?: RenderOptions
) => {
const queryClient = createTestQueryClient();
const providers = ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
return render(ui, { wrapper: providers, ...options });
};
export * from '@testing-library/react';
Mock Implementations
Create mock implementations for API clients:
// Mock API client
jest.mock('@/utils/api-client/tasks-client', () => ({
TasksClient: jest.fn().mockImplementation(() => ({
getTasks: jest.fn(),
createTask: jest.fn(),
updateTask: jest.fn(),
deleteTask: jest.fn(),
})),
}))
Testing Best Practices
Test Structure
✅ Good Practices
// Test user behavior, not implementation
expect(screen.getByRole('button')).toBeInTheDocument();
// Use semantic queries
screen.getByRole('button', { name: /submit/i });
// Test error states
expect(screen.getByText(/error/i)).toBeInTheDocument();
❌ Avoid These
// Don't test implementation details
expect(wrapper.find('.my-button')).toHaveLength(1);
// Avoid fragile queries
screen.getByClassName('button-submit');
// Don't test internal state
expect(component.state.isLoading).toBe(true);
Test Organization
Coverage Guidelines
Unit Tests
Component and utility function testing
Target: 80%+Integration Tests
Hook and API integration testing
Target: 70%+Critical Paths
Authentication and core workflows
Target: 90%+Edge Cases
Error handling and boundary conditions
Target: 60%+Integration with CI/CD
Pre-commit Hooks
Tests run automatically before commits through the validation script:
# apps/frontend/scripts/validate.sh
echo "🔍 Running tests..."
npm test -- --passWithNoTests --watchAll=false
TEST_EXIT_CODE=$?
# Validation continues with other checks...
if [ $TEST_EXIT_CODE -ne 0 ]; then
echo "✗ Tests failed"
exit 1
fi
GitHub Actions
Automated testing in CI/CD pipeline:
# .github/workflows/frontend.yml
test:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
cache-dependency-path: 'apps/frontend/package-lock.json'
- name: Install dependencies
run: npm ci
working-directory: ./apps/frontend
- name: Run tests
run: npm run test:ci
working-directory: ./apps/frontend
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./apps/frontend/coverage/lcov.info
Troubleshooting
Common Issues
Environment Variables Missing
# Add to jest.setup.js
process.env.NEXT_PUBLIC_API_BASE_URL = 'http://localhost:8080/api/v1';
Browser APIs Not Available
# Mock in jest.setup.js
global.matchMedia = jest.fn().mockImplementation(query => ({ ... }));
global.ResizeObserver = jest.fn();
Async Operations Not Awaited
// Use act() for async operations
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 0));
});
Debug Tips
# Run tests with verbose output
npm test -- --verbose
# Run specific test file
npm test Button.test.tsx
# Run tests matching pattern
npm test -- --testNamePattern="should render"
# Debug failing test
npm test -- --detectOpenHandles