Testing Guide¶
Comprehensive testing strategies for the Petfolio monorepo.
Testing Philosophy¶
We follow the testing pyramid approach:
graph TD
A[E2E Tests<br/>Few, High Value] --> B[Integration Tests<br/>Moderate Coverage]
B --> C[Unit Tests<br/>Comprehensive Coverage]
style A fill:#e57373
style B fill:#ffb74d
style C fill:#81c784
- Unit Tests: Test individual components and functions in isolation
- Integration Tests: Test how components work together
- E2E Tests: Test complete user workflows
Unit Testing with Jest¶
Running Tests¶
# Run all tests
npx nx run-many --target=test --all
# Test specific project
npx nx test ui-component-library
# Watch mode
npx nx test ui-component-library --watch
# Coverage report
npx nx test ui-component-library --coverage
Test Structure¶
Button.spec.tsx
import { render, screen } from '@testing-library/react';
import { Button } from './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 = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);
screen.getByText('Click me').click();
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('renders with variant styles', () => {
render(<Button variant="primary">Primary</Button>);
const button = screen.getByText('Primary');
expect(button).toHaveClass('variant-primary');
});
});
Testing Hooks¶
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
describe('useCounter', () => {
it('increments counter', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
});
Mocking¶
// Mock external dependencies
jest.mock('@util-global-theme', () => ({
useTheme: () => ({ palette: { primary: { main: '#000' } } }),
}));
// Mock API calls
jest.mock('./api', () => ({
fetchUser: jest.fn().mockResolvedValue({ id: 1, name: 'Test' }),
}));
E2E Testing with Playwright¶
Running E2E Tests¶
# Run all e2e tests
npx nx e2e petfolio-ui-tests
# Run specific test
npx nx e2e petfolio-ui-tests --spec=login.spec.ts
# Run in UI mode
npx nx e2e petfolio-ui-tests --ui
# Debug mode
npx nx e2e petfolio-ui-tests --debug
Test Structure¶
login.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Login Flow', () => {
test('should login successfully with valid credentials', async ({
page,
}) => {
await page.goto('/login');
await page.fill('[name="email"]', 'user@example.com');
await page.fill('[name="password"]', 'password123');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('h1')).toContainText('Dashboard');
});
test('should show error with invalid credentials', async ({ page }) => {
await page.goto('/login');
await page.fill('[name="email"]', 'invalid@example.com');
await page.fill('[name="password"]', 'wrong');
await page.click('button[type="submit"]');
await expect(page.locator('.error-message')).toBeVisible();
});
});
Best Practices¶
!!! tip "Use Data Attributes" Use data-testid for reliable selectors:
tsx <button data-testid="submit-button">Submit</button>
typescript await page.click('[data-testid="submit-button"]');
!!! tip "Wait for Navigation" Wait for navigation to complete:
typescript await Promise.all([ page.waitForNavigation(), page.click('button[type="submit"]'), ]);
Visual Testing with Storybook¶
Creating Stories¶
Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
title: 'Atoms/Button',
component: Button,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
variant: 'primary',
children: 'Primary Button',
},
};
export const Secondary: Story = {
args: {
variant: 'secondary',
children: 'Secondary Button',
},
};
Running Storybook¶
# Start Storybook
npx nx storybook ui-component-library
# Build static Storybook
npx nx build-storybook ui-component-library
Testing Patterns¶
Arrange-Act-Assert¶
test('should update user profile', async () => {
// Arrange
const user = { id: 1, name: 'John' };
const updateData = { name: 'Jane' };
// Act
const result = await updateUserProfile(user.id, updateData);
// Assert
expect(result.name).toBe('Jane');
});
Given-When-Then¶
test('should display error when form is invalid', () => {
// Given a form with validation rules
render(<ContactForm />);
// When submitting with empty fields
screen.getByRole('button', { name: /submit/i }).click();
// Then error messages should appear
expect(screen.getByText(/email is required/i)).toBeInTheDocument();
});
Coverage Goals¶
| Type | Target | Current |
|---|---|---|
| Unit Tests | 80%+ | TBD |
| Integration | 60%+ | TBD |
| E2E Critical Paths | 100% | TBD |
View coverage reports:
CI Testing¶
Tests run automatically in CI:
sequenceDiagram
participant Dev as Developer
participant GH as GitHub
participant CI as CI Pipeline
participant Report as Test Report
Dev->>GH: Push commit
GH->>CI: Trigger workflow
CI->>CI: Run unit tests
CI->>CI: Run e2e tests
CI->>Report: Generate coverage
Report-->>Dev: Show results in PR
Accessibility Testing¶
Storybook A11y Addon¶
Storybook includes accessibility testing:
- Open Storybook
- Select a story
- Click "Accessibility" tab
- Review violations
Manual A11y Testing¶
import { axe } from 'jest-axe';
test('should have no accessibility violations', async () => {
const { container } = render(<Button>Click me</Button>);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
Performance Testing¶
Component Performance¶
import { render } from '@testing-library/react';
test('should render quickly', () => {
const start = performance.now();
render(<ExpensiveComponent data={largeDataset} />);
const end = performance.now();
expect(end - start).toBeLessThan(100); // Should render in < 100ms
});
Debugging Tests¶
Debug in VS Code¶
- Set breakpoint in test
- Click "Debug" in test file
- Step through code
Playwright Debug¶
# Open Playwright Inspector
npx nx e2e petfolio-ui-tests --debug
# Generate trace
npx nx e2e petfolio-ui-tests --trace on
Common Testing Issues¶
Async Issues¶
// ❌ Bad - doesn't wait
test('loads data', () => {
render(<AsyncComponent />);
expect(screen.getByText('Data loaded')).toBeInTheDocument();
});
// ✅ Good - waits for element
test('loads data', async () => {
render(<AsyncComponent />);
await screen.findByText('Data loaded');
});
Theme Provider Issues¶
// Wrap components that use theme
import { ThemeProvider } from '@util-global-theme';
const renderWithTheme = (component: React.ReactElement) => {
return render(<ThemeProvider>{component}</ThemeProvider>);
};
test('renders with theme', () => {
renderWithTheme(<ThemedButton />);
});
Next Steps¶
- Development Workflows - Daily development tasks
- Docker Guide - Container testing
- Component Library - Component development