Testing Guide
This guide covers testing strategies, patterns, and best practices for the Nounspace codebase to ensure code quality and reliability.
Testing Philosophy
1. Testing Pyramid
- Unit Tests - Test individual components and functions in isolation
- Integration Tests - Test component interactions and data flow
- E2E Tests - Test complete user workflows and scenarios
- Visual Tests - Test visual appearance and behavior
2. Testing Principles
- Test Behavior - Test what the code does, not how it does it
- Test Isolation - Each test should be independent and isolated
- Test Clarity - Tests should be clear and easy to understand
- Test Coverage - Aim for high test coverage with meaningful tests
Testing Setup
1. Testing Framework
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
setupFiles: ['./tests/setup.ts'],
globals: true,
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
});
2. Test Setup
// tests/setup.ts
import '@testing-library/jest-dom';
import { cleanup } from '@testing-library/react';
import { afterEach } from 'vitest';
// Clean up after each test
afterEach(() => {
cleanup();
});
// Mock global objects
global.ResizeObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn(),
}));
// Mock IntersectionObserver
global.IntersectionObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn(),
}));
Unit Testing
1. Component Testing
// Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from '@/components/Button';
describe('Button', () => {
it('renders with correct text', () => {
render(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('calls onClick when clicked', () => {
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByText('Click me'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('is disabled when disabled prop is true', () => {
render(<Button disabled>Click me</Button>);
expect(screen.getByRole('button')).toBeDisabled();
});
it('applies correct variant classes', () => {
render(<Button variant="primary">Click me</Button>);
expect(screen.getByRole('button')).toHaveClass('btn-primary');
});
});
2. Hook Testing
// useCounter.test.ts
import { renderHook, act } from '@testing-library/react';
import { useCounter } from '@/hooks/useCounter';
describe('useCounter', () => {
it('should initialize with default value', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
});
it('should initialize with custom value', () => {
const { result } = renderHook(() => useCounter(5));
expect(result.current.count).toBe(5);
});
it('should increment count', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
it('should decrement count', () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(4);
});
it('should reset count', () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.reset();
});
expect(result.current.count).toBe(0);
});
});
3. Utility Function Testing
// utils.test.ts
import { formatDate, validateEmail, debounce } from '@/utils';
describe('formatDate', () => {
it('should format date correctly', () => {
const date = new Date('2023-12-25');
expect(formatDate(date)).toBe('Dec 25, 2023');
});
it('should handle invalid date', () => {
expect(formatDate(new Date('invalid'))).toBe('Invalid Date');
});
});
describe('validateEmail', () => {
it('should validate correct email', () => {
expect(validateEmail('test@example.com')).toBe(true);
});
it('should reject invalid email', () => {
expect(validateEmail('invalid-email')).toBe(false);
});
});
describe('debounce', () => {
it('should debounce function calls', async () => {
const mockFn = vi.fn();
const debouncedFn = debounce(mockFn, 100);
debouncedFn();
debouncedFn();
debouncedFn();
expect(mockFn).not.toHaveBeenCalled();
await new Promise(resolve => setTimeout(resolve, 150));
expect(mockFn).toHaveBeenCalledTimes(1);
});
});
Integration Testing
1. Component Integration
// UserProfile.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { UserProfile } from '@/components/UserProfile';
import { UserProvider } from '@/contexts/UserContext';
const mockUser = {
id: '1',
name: 'John Doe',
email: 'john@example.com',
role: 'user'
};
describe('UserProfile Integration', () => {
it('should display user information', () => {
render(
<UserProvider value={mockUser}>
<UserProfile />
</UserProvider>
);
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('john@example.com')).toBeInTheDocument();
});
it('should allow editing user information', async () => {
const mockUpdate = vi.fn();
render(
<UserProvider value={mockUser}>
<UserProfile onUpdate={mockUpdate} />
</UserProvider>
);
const editButton = screen.getByText('Edit');
fireEvent.click(editButton);
const nameInput = screen.getByLabelText('Name');
fireEvent.change(nameInput, { target: { value: 'Jane Doe' } });
const saveButton = screen.getByText('Save');
fireEvent.click(saveButton);
await waitFor(() => {
expect(mockUpdate).toHaveBeenCalledWith({
...mockUser,
name: 'Jane Doe'
});
});
});
});
2. Store Integration
// userStore.test.ts
import { renderHook, act } from '@testing-library/react';
import { create } from 'zustand';
import { userStore } from '@/stores/userStore';
describe('UserStore Integration', () => {
it('should manage user state', () => {
const { result } = renderHook(() => userStore());
act(() => {
result.current.setUser(mockUser);
});
expect(result.current.user).toEqual(mockUser);
expect(result.current.isAuthenticated).toBe(true);
});
it('should handle user logout', () => {
const { result } = renderHook(() => userStore());
act(() => {
result.current.setUser(mockUser);
});
expect(result.current.isAuthenticated).toBe(true);
act(() => {
result.current.logout();
});
expect(result.current.user).toBeNull();
expect(result.current.isAuthenticated).toBe(false);
});
});
End-to-End Testing
1. E2E Test Setup
// e2e/user-flow.spec.ts
import { test, expect } from '@playwright/test';
test.describe('User Authentication Flow', () => {
test('should allow user to login and access dashboard', async ({ page }) => {
// Navigate to login page
await page.goto('/login');
// Fill login form
await page.fill('[data-testid="email-input"]', 'test@example.com');
await page.fill('[data-testid="password-input"]', 'password123');
// Submit form
await page.click('[data-testid="login-button"]');
// Wait for redirect to dashboard
await page.waitForURL('/dashboard');
// Verify dashboard content
expect(await page.textContent('[data-testid="welcome-message"]')).toContain('Welcome');
});
});
2. E2E Test Scenarios
// e2e/space-creation.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Space Creation Flow', () => {
test('should create a new space', async ({ page }) => {
// Login first
await page.goto('/login');
await page.fill('[data-testid="email-input"]', 'test@example.com');
await page.fill('[data-testid="password-input"]', 'password123');
await page.click('[data-testid="login-button"]');
// Navigate to spaces
await page.goto('/spaces');
// Click create space button
await page.click('[data-testid="create-space-button"]');
// Fill space form
await page.fill('[data-testid="space-name-input"]', 'My New Space');
await page.fill('[data-testid="space-description-input"]', 'A test space');
// Submit form
await page.click('[data-testid="create-space-submit"]');
// Verify space was created
await page.waitForSelector('[data-testid="space-card"]');
expect(await page.textContent('[data-testid="space-name"]')).toBe('My New Space');
});
});
Visual Testing
1. Visual Regression Testing
// visual/button.visual.test.ts
import { test, expect } from '@playwright/test';
test.describe('Button Visual Tests', () => {
test('should render primary button correctly', async ({ page }) => {
await page.goto('/components/button');
const button = page.locator('[data-testid="primary-button"]');
await expect(button).toHaveScreenshot('primary-button.png');
});
test('should render secondary button correctly', async ({ page }) => {
await page.goto('/components/button');
const button = page.locator('[data-testid="secondary-button"]');
await expect(button).toHaveScreenshot('secondary-button.png');
});
});
2. Responsive Testing
// visual/responsive.visual.test.ts
import { test, expect } from '@playwright/test';
test.describe('Responsive Design Tests', () => {
test('should render correctly on mobile', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 });
await page.goto('/dashboard');
await expect(page).toHaveScreenshot('dashboard-mobile.png');
});
test('should render correctly on tablet', async ({ page }) => {
await page.setViewportSize({ width: 768, height: 1024 });
await page.goto('/dashboard');
await expect(page).toHaveScreenshot('dashboard-tablet.png');
});
test('should render correctly on desktop', async ({ page }) => {
await page.setViewportSize({ width: 1920, height: 1080 });
await page.goto('/dashboard');
await expect(page).toHaveScreenshot('dashboard-desktop.png');
});
});
Accessibility Testing
1. Accessibility Test Setup
// a11y/accessibility.test.ts
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test.describe('Accessibility Tests', () => {
test('should not have accessibility violations', async ({ page }) => {
await page.goto('/dashboard');
const accessibilityScanResults = await new AxeBuilder({ page }).analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
test('should be keyboard navigable', async ({ page }) => {
await page.goto('/dashboard');
// Test tab navigation
await page.keyboard.press('Tab');
const focusedElement = page.locator(':focus');
await expect(focusedElement).toBeVisible();
// Test arrow key navigation
await page.keyboard.press('ArrowDown');
await page.keyboard.press('ArrowUp');
});
});
2. Screen Reader Testing
// a11y/screen-reader.test.ts
import { test, expect } from '@playwright/test';
test.describe('Screen Reader Tests', () => {
test('should have proper ARIA labels', async ({ page }) => {
await page.goto('/dashboard');
// Check for ARIA labels
const elementsWithAriaLabels = page.locator('[aria-label]');
await expect(elementsWithAriaLabels).toHaveCount(5);
// Check for ARIA roles
const elementsWithRoles = page.locator('[role]');
await expect(elementsWithRoles).toHaveCount(3);
});
test('should have proper heading structure', async ({ page }) => {
await page.goto('/dashboard');
// Check heading hierarchy
const h1 = page.locator('h1');
const h2 = page.locator('h2');
const h3 = page.locator('h3');
await expect(h1).toHaveCount(1);
await expect(h2).toHaveCount(2);
await expect(h3).toHaveCount(3);
});
});
Performance Testing
1. Performance Metrics
// performance/performance.test.ts
import { test, expect } from '@playwright/test';
test.describe('Performance Tests', () => {
test('should load page within acceptable time', async ({ page }) => {
const startTime = Date.now();
await page.goto('/dashboard');
const loadTime = Date.now() - startTime;
expect(loadTime).toBeLessThan(3000); // 3 seconds
});
test('should have good Core Web Vitals', async ({ page }) => {
await page.goto('/dashboard');
const metrics = await page.evaluate(() => {
return new Promise((resolve) => {
new PerformanceObserver((list) => {
const entries = list.getEntries();
resolve(entries);
}).observe({ entryTypes: ['largest-contentful-paint', 'first-input', 'cumulative-layout-shift'] });
});
});
expect(metrics).toBeDefined();
});
});
2. Bundle Size Testing
// performance/bundle-size.test.ts
import { test, expect } from '@playwright/test';
test.describe('Bundle Size Tests', () => {
test('should have acceptable bundle size', async ({ page }) => {
await page.goto('/dashboard');
const bundleSize = await page.evaluate(() => {
return performance.getEntriesByType('resource')
.filter(entry => entry.name.includes('.js'))
.reduce((total, entry) => total + entry.transferSize, 0);
});
expect(bundleSize).toBeLessThan(500000); // 500KB
});
});
Test Data Management
1. Test Fixtures
// fixtures/user.fixtures.ts
export const mockUser = {
id: '1',
name: 'John Doe',
email: 'john@example.com',
role: 'user',
createdAt: '2023-01-01T00:00:00Z',
updatedAt: '2023-01-01T00:00:00Z'
};
export const mockUsers = [
mockUser,
{
id: '2',
name: 'Jane Doe',
email: 'jane@example.com',
role: 'admin',
createdAt: '2023-01-02T00:00:00Z',
updatedAt: '2023-01-02T00:00:00Z'
}
];
2. Test Utilities
// utils/test-utils.ts
import { render, RenderOptions } from '@testing-library/react';
import { ReactElement } from 'react';
import { UserProvider } from '@/contexts/UserContext';
const AllTheProviders = ({ children }: { children: React.ReactNode }) => {
return (
<UserProvider>
{children}
</UserProvider>
);
};
const customRender = (
ui: ReactElement,
options?: Omit<RenderOptions, 'wrapper'>
) => render(ui, { wrapper: AllTheProviders, ...options });
export * from '@testing-library/react';
export { customRender as render };
Test Automation
1. CI/CD Integration
# .github/workflows/test.yml
name: Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npm run test:unit
- name: Run integration tests
run: npm run test:integration
- name: Run E2E tests
run: npm run test:e2e
- name: Run accessibility tests
run: npm run test:a11y
2. Test Reporting
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'tests/',
'**/*.d.ts',
'**/*.config.*'
]
},
reporters: ['verbose', 'junit'],
outputFile: {
junit: './test-results/junit.xml'
}
}
});
Best Practices
1. Test Organization
- Group related tests in describe blocks
- Use descriptive test names that explain what is being tested
- Keep tests focused on a single behavior
- Use consistent naming conventions
2. Test Maintenance
- Update tests when code changes
- Remove obsolete tests that are no longer relevant
- Refactor tests to keep them maintainable
- Monitor test performance and optimize slow tests
3. Test Quality
- Write meaningful tests that catch real bugs
- Avoid testing implementation details focus on behavior
- Use appropriate test types for different scenarios
- Maintain good test coverage without sacrificing quality
4. Debugging Tests
- Use debugging tools like browser DevTools for E2E tests
- Add logging to understand test failures
- Use test utilities to simplify test setup
- Document test scenarios for complex tests