Testing Guide
This document explains how to run and write tests for the frontend application.
Quick Start
# Run all tests once
npm run test:run
# Run tests in watch mode (re-runs on file changes)
npm test
# Run tests with coverage report
npm run test:coverage
# Run tests with UI dashboard
npm run test:ui
# Run specific test file
npm test src/components/__tests__/LoadingSpinner.test.tsx
# Run tests matching a pattern
npm test -- --grep "should render"
Test Structure
Test Files Location
- Components:
src/components/__tests__/ComponentName.test.tsx - Utils:
src/utils/__tests__/utilName.test.ts - Lib:
src/lib/__tests__/libName.test.ts - Hooks:
src/hooks/__tests__/hookName.test.ts
Test Setup
Tests use Vitest with the following tools:
- @testing-library/react - React component testing
- @testing-library/jest-dom - DOM matchers
- @testing-library/user-event - User interaction simulation
- jsdom - DOM environment for tests
Writing Tests
1. Basic Component Test
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import { MyComponent } from '../MyComponent'
describe('MyComponent', () => {
it('should render correctly', () => {
render(<MyComponent text="Hello" />)
expect(screen.getByText('Hello')).toBeInTheDocument()
})
it('should handle click events', async () => {
const user = userEvent.setup()
const handleClick = vi.fn()
render(<MyComponent onClick={handleClick} />)
await user.click(screen.getByRole('button'))
expect(handleClick).toHaveBeenCalledOnce()
})
})
2. Testing Hooks
import { describe, it, expect } from 'vitest'
import { renderHook, act } from '@testing-library/react'
import { useCounter } from '../useCounter'
describe('useCounter', () => {
it('should increment counter', () => {
const { result } = renderHook(() => useCounter(0))
act(() => {
result.current.increment()
})
expect(result.current.count).toBe(1)
})
})
3. Testing Utilities
import { describe, it, expect } from 'vitest'
import { formatDate } from '../dateUtils'
describe('dateUtils', () => {
it('should format dates correctly', () => {
const date = new Date('2023-12-25')
expect(formatDate(date)).toBe('December 25, 2023')
})
})
4. Testing Async Functions
import { describe, it, expect, vi } from 'vitest'
import { fetchUserData } from '../api'
// Mock fetch
global.fetch = vi.fn()
describe('fetchUserData', () => {
it('should fetch user data successfully', async () => {
const mockData = { id: 1, name: 'John' }
fetch.mockResolvedValueOnce({
ok: true,
json: async () => mockData,
})
const result = await fetchUserData(1)
expect(result).toEqual(mockData)
})
})
Mocking
Global Mocks (Available Automatically)
- Firebase - Mocked in
src/test/setup.ts - Next.js Router - Mocked navigation functions
- Langfuse - Mocked tracing functionality
- fetch - Global fetch mock
- IntersectionObserver - Mocked for component tests
- ResizeObserver - Mocked for component tests
Custom Mocks in Tests
import { vi } from 'vitest'
// Mock a module
vi.mock('../api', () => ({
fetchData: vi.fn().mockResolvedValue({ data: 'test' })
}))
// Mock a function
const mockFunction = vi.fn()
mockFunction.mockReturnValue('mocked return')
// Mock with different implementations
mockFunction
.mockReturnValueOnce('first call')
.mockReturnValueOnce('second call')
.mockReturnValue('default')
Common Testing Patterns
1. Testing Form Components
it('should submit form with correct data', async () => {
const user = userEvent.setup()
const handleSubmit = vi.fn()
render(<ContactForm onSubmit={handleSubmit} />)
await user.type(screen.getByLabelText(/name/i), 'John Doe')
await user.type(screen.getByLabelText(/email/i), 'john@example.com')
await user.click(screen.getByRole('button', { name: /submit/i }))
expect(handleSubmit).toHaveBeenCalledWith({
name: 'John Doe',
email: 'john@example.com'
})
})
2. Testing Loading States
it('should show loading spinner while fetching', async () => {
render(<UserProfile userId="123" />)
expect(screen.getByText(/loading/i)).toBeInTheDocument()
await waitFor(() => {
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument()
})
})
3. Testing Error Handling
it('should display error message on fetch failure', async () => {
fetch.mockRejectedValueOnce(new Error('Network error'))
render(<UserProfile userId="123" />)
await waitFor(() => {
expect(screen.getByText(/error loading user/i)).toBeInTheDocument()
})
})
4. Testing Component Props
it('should apply custom className', () => {
const { container } = render(<Button className="custom-class" />)
expect(container.firstElementChild).toHaveClass('custom-class')
})
Best Practices
1. Test Behavior, Not Implementation
// ❌ Bad - testing implementation
expect(component.state.loading).toBe(true)
// ✅ Good - testing behavior
expect(screen.getByText(/loading/i)).toBeInTheDocument()
2. Use Descriptive Test Names
// ❌ Bad
it('should work', () => {})
// ✅ Good
it('should display error message when API call fails', () => {})
3. Use Screen Queries Appropriately
// ✅ Preferred queries (in order)
screen.getByRole('button', { name: /submit/i }) // Most accessible
screen.getByLabelText(/email/i) // For form inputs
screen.getByText(/welcome/i) // For text content
screen.getByTestId('submit-button') // Last resort
4. Clean Up After Tests
afterEach(() => {
vi.clearAllMocks() // Clear mock calls
cleanup() // Clean up DOM (automatic with setup)
})
5. Group Related Tests
describe('UserProfile', () => {
describe('when user exists', () => {
it('should display user name', () => {})
it('should display user email', () => {})
})
describe('when user does not exist', () => {
it('should display not found message', () => {})
})
})
Configuration Files
- vitest.config.ts - Main Vitest configuration
- src/test/setup.ts - Test setup and global mocks
- package.json - Test scripts
Coverage Reports
Generate coverage reports to see what code is tested:
npm run test:coverage
Coverage reports will be generated in the coverage/ directory and displayed in the terminal.
CI/CD Integration
Add this to your GitHub Actions workflow:
- name: Run Tests
run: npm run test:run
- name: Generate Coverage
run: npm run test:coverage
Example Test Files
See these files for examples:
src/utils/__tests__/vacChat.test.ts- Complex async testing with mockssrc/components/__tests__/LoadingSpinner.test.tsx- Component testingsrc/lib/__tests__/utils.test.ts- Utility function testing
Debugging Tests
Run Single Test
npm test -- --grep "specific test name"
Enable Debugging
npm test -- --reporter=verbose
Use Test UI
npm run test:ui
This opens a web interface where you can see test results, coverage, and debug failing tests.
Common Errors and Solutions
1. “ReferenceError: HTMLElement is not defined”
- Solution: Make sure
environment: 'jsdom'is set in vitest.config.ts
2. “Cannot find module ‘@testing-library/jest-dom’”
- Solution: Import is handled in
src/test/setup.ts, make sure it’s configured correctly
3. “fetch is not defined”
- Solution: fetch is mocked globally in
src/test/setup.ts
4. Tests are flaky or timing out
- Solution: Use
waitForfor async operations and increase timeout if needed
Happy testing! 🧪