Testing Strategies for Frontend Teams
Finding the right balance between test coverage and development velocity is crucial for frontend teams.
The Testing Pyramid
/\
/E2E\
/------\
/ Integ \
/----------\
/ Unit \
/--------------\
Unit Tests (70%)
Fast, isolated tests for individual functions and components.
Integration Tests (20%)
Test how components work together.
E2E Tests (10%)
Full user workflows from UI to backend.
Unit Testing Components
Use React Testing Library for better maintainability:
import { render, screen } from '@testing-library/react';
import { ProjectCard } from './ProjectCard';
test('renders project title and description', () => {
render(
title="My Project"
description="A cool project"
/>
);
expect(screen.getByText('My Project')).toBeInTheDocument();
expect(screen.getByText('A cool project')).toBeInTheDocument();
});
Integration Testing
Test component interactions and data flow:
test('filters projects by tag', async () => {
render( );
const filterButton = screen.getByRole('button', { name: 'Next.js' });
await user.click(filterButton);
const projects = screen.getAllByRole('article');
expect(projects).toHaveLength(2); // Only Next.js projects
});
E2E Testing with Playwright
Critical user journeys only:
import { test, expect } from '@playwright/test';
test('user can submit contact form', async ({ page }) => {
await page.goto('/');
await page.click('a[href="#contact"]');
await page.fill('input[name="name"]', 'John Doe');
await page.fill('input[name="email"]', 'john@example.com');
await page.fill('textarea[name="message"]', 'Hello!');
await page.click('button[type="submit"]');
await expect(page.locator('text=Message sent')).toBeVisible();
});
Best Practices
1. Test Behavior, Not Implementation
❌ Bad: expect(component.state.counter).toBe(5)
✅ Good: expect(screen.getByText('Count: 5')).toBeInTheDocument()
2. Use Data-Testid Sparingly
Prefer semantic queries (role, label, text) over test IDs.
3. Mock External Dependencies
Mock API calls, third-party services, and heavy computations.
vi.mock('@/lib/api', () => ({
fetchProjects: vi.fn(() => Promise.resolve(mockProjects))
}));
4. Snapshot Tests with Caution
They break often and can create false confidence. Use for:
Error messages
Complex computed output
Generated HTML/emails
Continuous Integration
Run tests on every PR:
name: Tests
on: [pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: npm ci
- run: npm test
- run: npm run test:e2e
Metrics to Track
**Code coverage**: Aim for 80%+ but don't obsess
**Test execution time**: Keep under 5 minutes
**Flaky test rate**: Should be < 1%
Conclusion
Good testing isn't about 100% coverage—it's about confidence to ship. Focus on critical paths and maintain fast feedback loops.
