, or method calls. Tests should interact with the component solely through the DOM, mimicking user actions. This ensures tests remain valid during refactors, as long as the user-facing behavior remains unchanged.
Implementation:
Use testing utilities that enforce accessibility-aware queries. For React ecosystems, @testing-library/react combined with @testing-library/user-event is the standard.
// src/components/LoginForm.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginForm } from './LoginForm';
import { vi } from 'vitest';
describe('LoginForm', () => {
it('submits credentials and handles success response', async () => {
const onSuccess = vi.fn();
render(<LoginForm onSuccess={onSuccess} />);
const user = userEvent.setup();
// User-centric queries
const emailInput = screen.getByLabelText('Email');
const passwordInput = screen.getByLabelText('Password');
const submitButton = screen.getByRole('button', { name: /sign in/i });
await user.type(emailInput, 'dev@codcompass.io');
await user.type(passwordInput, 'secure-password');
await user.click(submitButton);
// Assert loading state
expect(screen.getByRole('progressbar')).toBeInTheDocument();
// Assert success state and callback
await screen.findByText('Welcome back!');
expect(onSuccess).toHaveBeenCalledWith({ email: 'dev@codcompass.io' });
});
});
Step 2: Implement Network Interception for Determinism
Mocking API calls at the HTTP layer is superior to mocking fetch/axios directly or mocking React hooks. HTTP-level mocking ensures that your components handle real serialization, headers, and network errors, while keeping tests deterministic and fast.
Tooling:
Use Mock Service Worker (MSW). MSW operates as a Service Worker, intercepting requests before they reach the network. This allows the same mocks to be used in unit, integration, and E2E tests.
// src/mocks/handlers.ts
import { http, HttpResponse } from 'msw';
export const handlers = [
http.post('/api/auth/login', async ({ request }) => {
const body = await request.json();
if (body.email === 'dev@codcompass.io' && body.password === 'secure-password') {
return HttpResponse.json({ token: 'mock-jwt', user: { id: 1 } });
}
return HttpResponse.json(
{ error: 'Invalid credentials' },
{ status: 401 }
);
}),
];
Tests must be isolated. Global state, local storage, and service workers must be reset between tests to prevent cross-contamination.
Architecture Decision:
Use a dedicated test setup file to configure global mocks and cleanup. For Vitest, leverage globalSetup and setupFiles.
// src/setupTests.ts
import { afterAll, afterEach, beforeAll } from 'vitest';
import { setupWorker } from 'msw/browser';
import { handlers } from './mocks/handlers';
const worker = setupWorker(...handlers);
beforeAll(() => {
worker.start({ onUnhandledRequest: 'bypass' });
});
afterEach(() => {
worker.resetHandlers();
// Clear local storage, reset DOM, etc.
document.body.innerHTML = '';
});
afterAll(() => worker.stop());
Step 4: Visual Regression Testing Strategy
Functional tests do not catch CSS regressions, layout shifts, or accessibility violations in rendering. Integrate visual regression testing for UI components.
Tooling:
For component libraries, use Chromatic or Percy. For full-page snapshots, use Playwright's screenshot capabilities.
Implementation:
Create a visual test that captures component states.
// src/components/Button.visual.test.tsx
import { test, expect } from '@playwright/experimental-ct-react';
import { Button } from './Button';
test('Button renders correctly in all variants', async ({ mount }) => {
const variants = ['primary', 'secondary', 'danger'];
for (const variant of variants) {
const component = await mount(<Button variant={variant}>Action</Button>);
await expect(component).toHaveScreenshot(`button-${variant}.png`);
}
});
Step 5: CI/CD Integration and Parallelization
Optimize CI pipelines by categorizing tests. Run unit and integration tests on every commit. Run E2E and visual tests on pull requests or nightly builds, depending on cost constraints.
Pipeline Strategy:
- Commit Hook: Lint + Type Check + Fast Unit Tests (< 30s).
- PR Build: Integration Tests + Visual Regression + E2E Critical Path.
- Merge: Full E2E Suite + Performance Budget Checks.
Use test sharding to parallelize execution. Tools like Vitest and Playwright support built-in sharding to distribute tests across multiple CI runners, reducing wall-clock time linearly with runner count.
Pitfall Guide
1. Testing Implementation Details
Mistake: Asserting on component state, internal methods, or specific prop values passed to children.
Impact: Tests break during refactors even when user behavior is unchanged, leading to "test rot" and developer frustration.
Best Practice: Query by role, label, or text. Assert on output (DOM changes, network requests) rather than input or internal state.
2. Flaky E2E Tests Due to Race Conditions
Mistake: Using hardcoded sleeps or polling without proper waiting strategies in E2E tests.
Impact: CI becomes unreliable. Developers ignore failures, masking real bugs.
Best Practice: Use auto-waiting assertions provided by tools like Playwright. Ensure tests wait for network idle or specific element states before proceeding. Avoid sleep commands.
3. Over-Mocking Dependencies
Mistake: Mocking entire libraries or complex hooks in unit tests, effectively testing the mock rather than the code.
Impact: False sense of security. Integration failures are missed because the mock hides interface mismatches.
Best Practice: Mock only external boundaries (APIs, third-party services). Test interactions with real dependencies where possible, or use "shallow" mocking that preserves the interface contract.
4. Ignoring Accessibility in Tests
Mistake: Treating accessibility as a manual QA step rather than an automated check.
Impact: Legal risk and exclusion of users with disabilities. Accessibility bugs are often harder to fix later in the lifecycle.
Best Practice: Integrate axe-core or jest-axe into the test suite. Ensure testing library queries (which rely on ARIA roles) pass, as they inherently validate accessibility structure.
5. Snapshot Testing Abuse
Mistake: Using snapshots for entire component trees without reviewing diffs, or updating snapshots blindly.
Impact: Snapshots capture bugs. If a component renders incorrectly, the snapshot updates to match the bug, and the test passes.
Best Practice: Use snapshots only for stable, complex data structures or non-UI outputs. For UI, use visual regression tools or behavioral assertions. Always review snapshot diffs in code review.
6. Inconsistent Test Data Management
Mistake: Hardcoding data in tests or using random data without seeds.
Impact: Tests become brittle or non-reproducible. Debugging failures is difficult when data varies.
Best Practice: Use factories (e.g., factory-bot) to generate test data. Seed random generators for reproducibility. Centralize test data definitions to ensure consistency across the suite.
7. Running All Tests Everywhere
Mistake: Executing the full E2E suite on every commit or running slow visual tests on every PR.
Impact: Developer productivity plummets due to long feedback loops.
Best Practice: Implement test tagging. Run fast tests locally. Run comprehensive suites only when necessary. Use "changed file" detection to run only relevant tests during development.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Startup / MVP | Integration Tests + Critical E2E | Fast feedback, low maintenance, covers core flows. | Low setup cost; minimal CI compute. |
| Enterprise / Compliance | Full Testing Trophy + Visual + A11y | High reliability required; audit trails for accessibility and visual regression. | High initial setup; moderate CI cost due to parallelization. |
| Component Library | Unit + Visual Regression + Storybook | Components need strict API contracts and visual consistency across versions. | Medium cost; visual tools may have licensing fees. |
| High-Traffic App | E2E Critical Path + Performance Tests | Focus on user journeys that impact revenue; monitor performance budgets. | High E2E cost; offset by reduced production incidents. |
| Legacy Codebase | Characterization Tests + Integration | Stabilize existing behavior before refactoring; add integration tests for new features. | Medium cost; characterization tests can be voluminous initially. |
Configuration Template
Vitest Configuration with MSW and Testing Library
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/setupTests.ts'],
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: ['src/setupTests.ts', 'src/mocks/**', '**/*.d.ts'],
},
// Parallel execution settings
pool: 'threads',
poolOptions: {
threads: {
maxThreads: 4,
minThreads: 1,
},
},
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
});
Quick Start Guide
-
Initialize Project:
npm create vitest@latest
# Select React, TypeScript, and jsdom environment
-
Install Dependencies:
npm install -D @testing-library/react @testing-library/user-event msw
npx msw init public/ --save
-
Create Setup File:
Create src/setupTests.ts with MSW worker initialization and cleanup logic as shown in the Core Solution.
-
Write First Test:
Create src/App.test.tsx and write a behavioral test using render and screen queries.
-
Run Tests:
npm run test
# Or watch mode
npm run test:watch
This strategy provides a scalable, maintainable foundation for frontend testing that balances developer experience with production reliability. By prioritizing integration tests, enforcing behavioral patterns, and optimizing CI execution, teams can achieve high confidence in their releases without sacrificing velocity.