PatternsTesting Strategies
Testing Strategies
Comprehensive testing strategies for building reliable, maintainable frontend applications
Testing Strategies
Testing is easy. Testing well is hard. This section covers advanced testing strategies that go beyond basic unit tests to build truly reliable applications.
Why Advanced Testing Strategies Matter
Common Testing Mistakes:
- Only testing happy paths
- Ignoring edge cases
- No visual regression testing
- Skipping accessibility tests
- Poor test maintainability
- Slow, flaky tests
What This Section Covers: Advanced techniques that professional teams use to ship reliable software with confidence.
Testing Pyramid (Modern)
┌─────────────┐
│ E2E Tests │ (Few, critical flows)
│ 5-10% │
├─────────────┤
│ Integration │ (Feature-level)
│ 20-30% │
├─────────────┤
│ Unit │ (Functions, logic)
│ 60-70% │
└─────────────┘But also include:
- Visual regression tests
- Accessibility tests
- Performance tests
- Contract tests
- Mutation tests
Testing Philosophy
1. Test Behavior, Not Implementation
// ❌ BAD: Testing implementation details
test('uses useState hook', () => {
const wrapper = shallow(<Counter />);
expect(wrapper.find('useState')).toBeDefined();
});
// ✅ GOOD: Testing behavior
test('increments counter when button clicked', () => {
render(<Counter />);
const button = screen.getByRole('button', { name: /increment/i });
fireEvent.click(button);
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});2. Write Tests Users Would Write
// ❌ BAD: Testing like a developer
test('input value updates state', () => {
const { container } = render(<SearchBox />);
const input = container.querySelector('[data-testid="search-input"]');
// ...
});
// ✅ GOOD: Testing like a user
test('shows search results when user types', async () => {
render(<SearchBox />);
const user = userEvent.setup();
const searchBox = screen.getByRole('searchbox');
await user.type(searchBox, 'react');
expect(await screen.findByText('React Documentation')).toBeVisible();
});3. Test at the Right Level
// Unit test: Pure function
test('calculates total price correctly', () => {
expect(calculateTotal([10, 20, 30])).toBe(60);
});
// Integration test: Feature
test('checkout flow calculates shipping and tax', () => {
render(<CheckoutFlow items={mockItems} />);
// Test the full feature
});
// E2E test: Critical path
test('user can complete purchase', () => {
// Test end-to-end flow including API calls
});Testing Coverage Strategy
| Type | Coverage | Purpose | Speed |
|---|---|---|---|
| Unit | 60-70% | Business logic, utils | ⚡ Very Fast |
| Integration | 20-30% | Features, components | 🚀 Fast |
| E2E | 5-10% | Critical user flows | 🐌 Slow |
| Visual | Key pages | UI consistency | ⚡ Fast |
| A11y | All pages | Accessibility | ⚡ Fast |
| Performance | Key flows | Speed metrics | 🐌 Slow |
| Contract | API boundaries | API compatibility | 🚀 Fast |
What to Test
Must Test ✅
- Critical user flows (signup, checkout, auth)
- Business logic
- Error handling
- Accessibility
- Edge cases
- Security boundaries
Should Test ⚠️
- Component interactions
- Form validation
- Navigation
- State management
- API integration
- Loading states
Don't Test ❌
- Third-party libraries
- Implementation details
- Trivial code
- Framework internals
- CSS styling (use visual regression instead)
Test Organization
tests/
├── unit/
│ ├── utils/
│ │ ├── formatPrice.test.ts
│ │ └── validation.test.ts
│ └── hooks/
│ └── useAuth.test.ts
│
├── integration/
│ ├── features/
│ │ ├── Checkout.test.tsx
│ │ └── Search.test.tsx
│ └── components/
│ └── ProductCard.test.tsx
│
├── e2e/
│ ├── auth/
│ │ ├── login.spec.ts
│ │ └── signup.spec.ts
│ └── checkout/
│ └── complete-purchase.spec.ts
│
├── visual/
│ └── Homepage.visual.ts
│
├── a11y/
│ └── Dashboard.a11y.test.ts
│
└── performance/
└── product-list.perf.test.tsTesting Tools Ecosystem
Testing Frameworks
- Jest: Unit and integration tests
- Vitest: Faster Jest alternative
- Playwright: E2E testing
- Cypress: Alternative E2E
Component Testing
- React Testing Library: Component behavior
- Testing Library: Framework-agnostic
Visual Testing
- Percy: Visual regression
- Chromatic: Storybook visual testing
- BackstopJS: Self-hosted
Accessibility
- axe-core: A11y testing
- Pa11y: Automated accessibility
- jest-axe: Jest integration
Performance
- Lighthouse: Performance audits
- k6: Load testing
- Artillery: API load testing
Contract Testing
- Pact: Consumer-driven contracts
- MSW: API mocking
Mutation Testing
- Stryker: Test quality measurement
Testing Best Practices
1. Arrange-Act-Assert (AAA)
test('adds item to cart', () => {
// Arrange: Setup
render(<Cart />);
const addButton = screen.getByRole('button', { name: /add to cart/i });
// Act: Perform action
fireEvent.click(addButton);
// Assert: Verify result
expect(screen.getByText('1 item in cart')).toBeInTheDocument();
});2. Test Isolation
// ❌ BAD: Tests depend on each other
test('creates user', () => {
globalUser = createUser();
});
test('updates user', () => {
updateUser(globalUser); // Depends on previous test
});
// ✅ GOOD: Each test is independent
test('creates user', () => {
const user = createUser();
expect(user).toBeDefined();
});
test('updates user', () => {
const user = createUser(); // Create fresh user
updateUser(user);
expect(user.updated).toBe(true);
});3. Descriptive Test Names
// ❌ BAD
test('test 1', () => {});
test('works', () => {});
// ✅ GOOD
test('displays error message when email is invalid', () => {});
test('disables submit button while form is submitting', () => {});
test('redirects to login page when user is not authenticated', () => {});4. Use Test Helpers
// test-utils.tsx
import { render } from '@testing-library/react';
import { ThemeProvider } from '@/components/ThemeProvider';
import { AuthProvider } from '@/contexts/AuthContext';
export function renderWithProviders(ui: React.ReactElement, options = {}) {
return render(
<ThemeProvider>
<AuthProvider>
{ui}
</AuthProvider>
</ThemeProvider>,
options
);
}
// Usage in tests
test('displays user profile', () => {
renderWithProviders(<UserProfile />);
// ...
});5. Mock External Dependencies
// Mock fetch API
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: async () => ({ data: 'mock data' }),
})
) as jest.Mock;
// Mock router
jest.mock('next/navigation', () => ({
useRouter: () => ({
push: jest.fn(),
pathname: '/test',
}),
}));Common Testing Patterns
Testing Async Operations
test('loads and displays data', async () => {
render(<DataComponent />);
// Wait for loading to finish
await waitFor(() => {
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
});
// Check data is displayed
expect(screen.getByText('Data loaded')).toBeInTheDocument();
});Testing User Interactions
test('submits form with valid data', async () => {
const user = userEvent.setup();
const onSubmit = jest.fn();
render(<Form onSubmit={onSubmit} />);
await user.type(screen.getByLabelText('Email'), 'test@example.com');
await user.type(screen.getByLabelText('Password'), 'password123');
await user.click(screen.getByRole('button', { name: /submit/i }));
expect(onSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123',
});
});Testing Error States
test('displays error when API call fails', async () => {
// Mock failed API call
server.use(
rest.get('/api/data', (req, res, ctx) => {
return res(ctx.status(500), ctx.json({ error: 'Server error' }));
})
);
render(<DataComponent />);
expect(await screen.findByText('Failed to load data')).toBeInTheDocument();
});CI/CD Integration
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: '20'
- 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
- name: Upload coverage
uses: codecov/codecov-action@v3Next Steps
Explore specific testing strategies:
- Visual Regression Testing: Percy, Chromatic
- E2E Testing Patterns: Playwright patterns
- Component Testing: Testing Library
- Performance Testing: Load testing
- A11y Testing Automation: Axe, Pa11y
- Contract Testing: Pact for APIs
- Mutation Testing: Stryker
Remember: Testing is easy. Testing well is hard. Master these strategies to build confidence in your code.