Skip to main content

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