Frontend Testing Implementation Stack
1. Jest Vitest Unit Testing Framework
| Framework | Speed | Features | Use Case |
|---|---|---|---|
| Jest | Good | Snapshot testing, mocking, coverage, jsdom | Most popular, React default, comprehensive ecosystem |
| Vitest HOT | Excellent (5-10x faster) | Vite-powered, Jest-compatible API, ESM native | Modern projects, Vite ecosystem, faster CI/CD |
| Mocha + Chai | Good | Flexible, BDD/TDD, require plugins | Node.js testing, flexible assertion libraries |
| AVA | Excellent (parallel) | Concurrent tests, minimal API, fast | Node.js, simple tests, parallel execution |
Example: Jest & Vitest Unit Tests
// Jest configuration
// jest.config.js
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
moduleNameMapper: {
"\\.(css|less|scss|sass)$": "identity-obj-proxy",
"^@/(.*)$": "<rootDir>/src/$1"
},
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.d.ts',
'!src/**/*.stories.tsx'
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
};
// jest.setup.js
import '@testing-library/jest-dom';
// Basic unit test
// sum.test.ts
import { sum } from './sum';
describe('sum', () => {
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
test('adds negative numbers', () => {
expect(sum(-1, -2)).toBe(-3);
});
});
// Async testing
// fetchUser.test.ts
import { fetchUser } from './api';
describe('fetchUser', () => {
test('fetches user successfully', async () => {
const user = await fetchUser(1);
expect(user).toEqual({
id: 1,
name: 'John Doe'
});
});
test('throws error for invalid id', async () => {
await expect(fetchUser(-1)).rejects.toThrow('Invalid user ID');
});
});
// Mocking
// userService.test.ts
import { getUserById } from './userService';
import * as api from './api';
jest.mock('./api');
describe('getUserById', () => {
test('returns user data', async () => {
const mockUser = { id: 1, name: 'John' };
(api.fetchUser as jest.Mock).mockResolvedValue(mockUser);
const user = await getUserById(1);
expect(user).toEqual(mockUser);
expect(api.fetchUser).toHaveBeenCalledWith(1);
});
});
// Snapshot testing
// Button.test.tsx
import { render } from '@testing-library/react';
import { Button } from './Button';
test('renders button correctly', () => {
const { container } = render(<Button>Click me</Button>);
expect(container.firstChild).toMatchSnapshot();
});
// Vitest configuration
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './vitest.setup.ts',
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'src/**/*.test.{ts,tsx}'
]
}
}
});
// Vitest test (Jest-compatible API)
// calculator.test.ts
import { describe, test, expect, vi } from 'vitest';
import { Calculator } from './Calculator';
describe('Calculator', () => {
test('adds two numbers', () => {
const calc = new Calculator();
expect(calc.add(2, 3)).toBe(5);
});
test('mocks function call', () => {
const mockFn = vi.fn(() => 42);
expect(mockFn()).toBe(42);
expect(mockFn).toHaveBeenCalledTimes(1);
});
});
// Testing hooks
// useCounter.test.ts
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
describe('useCounter', () => {
test('increments counter', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
});
// Parameterized tests
describe.each([
[1, 1, 2],
[2, 2, 4],
[3, 3, 6]
])('sum(%i, %i)', (a, b, expected) => {
test(`returns ${expected}`, () => {
expect(sum(a, b)).toBe(expected);
});
});
// Timer mocking
test('calls callback after 1 second', () => {
jest.useFakeTimers();
const callback = jest.fn();
setTimeout(callback, 1000);
expect(callback).not.toBeCalled();
jest.runAllTimers();
expect(callback).toBeCalled();
expect(callback).toHaveBeenCalledTimes(1);
jest.useRealTimers();
});
// Module mocking
jest.mock('axios');
import axios from 'axios';
test('fetches data from API', async () => {
const mockData = { data: { id: 1 } };
(axios.get as jest.Mock).mockResolvedValue(mockData);
const result = await fetchData();
expect(result).toEqual(mockData.data);
});
// Run tests
npm test // Run all tests
npm test -- --watch // Watch mode
npm test -- --coverage // With coverage
npm test -- Button.test.tsx // Specific file
vitest // Vitest watch mode
vitest run // Vitest single run
Modern Choice: Use Vitest for new Vite projects (5-10x faster).
Use Jest for existing projects or Create React App.
2. React Testing Library Vue Test Utils
| Library | Framework | Philosophy | Key Features |
|---|---|---|---|
| React Testing Library | React | Test user behavior, not implementation | Query by role/text, user-event, accessibility |
| Vue Test Utils | Vue | Official Vue testing utility | Mount, shallow, wrapper API, composition API |
| Enzyme | React (legacy) | Shallow rendering, implementation testing | Deprecated, use RTL instead |
| Testing Library (core) | Framework agnostic | Base for all Testing Library variants | Angular, Svelte, Preact support |
Example: Component Testing
// React Testing Library
// Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from './Button';
describe('Button', () => {
test('renders button with text', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument();
});
test('calls onClick when clicked', async () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);
const button = screen.getByRole('button');
await userEvent.click(button);
expect(handleClick).toHaveBeenCalledTimes(1);
});
test('is disabled when disabled prop is true', () => {
render(<Button disabled>Click me</Button>);
expect(screen.getByRole('button')).toBeDisabled();
});
});
// Form testing
// LoginForm.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginForm } from './LoginForm';
test('submits form with username and password', async () => {
const handleSubmit = jest.fn();
render(<LoginForm onSubmit={handleSubmit} />);
const usernameInput = screen.getByLabelText(/username/i);
const passwordInput = screen.getByLabelText(/password/i);
const submitButton = screen.getByRole('button', { name: /login/i });
await userEvent.type(usernameInput, 'john@example.com');
await userEvent.type(passwordInput, 'password123');
await userEvent.click(submitButton);
await waitFor(() => {
expect(handleSubmit).toHaveBeenCalledWith({
username: 'john@example.com',
password: 'password123'
});
});
});
// Async component testing
// UserProfile.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import { UserProfile } from './UserProfile';
import { fetchUser } from './api';
jest.mock('./api');
test('displays user data after loading', async () => {
const mockUser = { name: 'John Doe', email: 'john@example.com' };
(fetchUser as jest.Mock).mockResolvedValue(mockUser);
render(<UserProfile userId={1} />);
// Loading state
expect(screen.getByText(/loading/i)).toBeInTheDocument();
// Wait for data to load
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
expect(screen.getByText('john@example.com')).toBeInTheDocument();
});
// Query priority (recommended order)
// 1. getByRole (most accessible)
screen.getByRole('button', { name: /submit/i });
// 2. getByLabelText (for forms)
screen.getByLabelText(/username/i);
// 3. getByPlaceholderText
screen.getByPlaceholderText(/enter email/i);
// 4. getByText
screen.getByText(/welcome/i);
// 5. getByDisplayValue (for inputs with value)
screen.getByDisplayValue('John Doe');
// 6. getByAltText (for images)
screen.getByAltText(/profile picture/i);
// 7. getByTitle
screen.getByTitle(/close/i);
// 8. getByTestId (last resort)
screen.getByTestId('custom-element');
// Custom render with providers
// test-utils.tsx
import { render } from '@testing-library/react';
import { ThemeProvider } from 'styled-components';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const AllTheProviders = ({ children }) => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false }
}
});
return (
<QueryClientProvider client={queryClient}>
<ThemeProvider theme={theme}>
{children}
</ThemeProvider>
</QueryClientProvider>
);
};
export const customRender = (ui, options) => {
return render(ui, { wrapper: AllTheProviders, ...options });
};
// Usage
import { customRender as render } from './test-utils';
test('renders with providers', () => {
render(<MyComponent />);
// Test code...
});
// Vue Test Utils
// Button.spec.ts
import { mount } from '@vue/test-utils';
import Button from './Button.vue';
describe('Button', () => {
test('renders button with text', () => {
const wrapper = mount(Button, {
props: { label: 'Click me' }
});
expect(wrapper.text()).toContain('Click me');
});
test('emits click event', async () => {
const wrapper = mount(Button);
await wrapper.trigger('click');
expect(wrapper.emitted()).toHaveProperty('click');
expect(wrapper.emitted('click')).toHaveLength(1);
});
});
// Vue with Composition API
// Counter.spec.ts
import { mount } from '@vue/test-utils';
import Counter from './Counter.vue';
test('increments count', async () => {
const wrapper = mount(Counter);
expect(wrapper.text()).toContain('Count: 0');
await wrapper.find('button').trigger('click');
expect(wrapper.text()).toContain('Count: 1');
});
// Testing slots
test('renders slot content', () => {
const wrapper = mount(Card, {
slots: {
default: '<div>Slot content</div>'
}
});
expect(wrapper.text()).toContain('Slot content');
});
Testing Philosophy: "The more your tests resemble the way your software
is used, the more confidence they can give you." - Kent C. Dodds (RTL creator)
3. Cypress Playwright E2E Automation
| Framework | Features | Speed | Use Case |
|---|---|---|---|
| Cypress | Time-travel debugging, real-time reload, screenshots | Good | E2E testing, developer-friendly, great DX |
| Playwright HOT | Multi-browser, parallel, mobile emulation, traces | Excellent | Cross-browser E2E, modern automation, Microsoft-backed |
| Selenium | Multi-language, WebDriver protocol, mature | Slow | Legacy projects, multi-language teams |
| Puppeteer | Chrome DevTools Protocol, headless Chrome | Good | Chrome-only automation, web scraping |
Example: E2E Testing
// Cypress
// cypress/e2e/login.cy.ts
describe('Login Flow', () => {
beforeEach(() => {
cy.visit('/login');
});
it('allows user to login successfully', () => {
cy.get('input[name="email"]').type('john@example.com');
cy.get('input[name="password"]').type('password123');
cy.get('button[type="submit"]').click();
// Assertions
cy.url().should('include', '/dashboard');
cy.contains('Welcome, John').should('be.visible');
});
it('shows error for invalid credentials', () => {
cy.get('input[name="email"]').type('invalid@example.com');
cy.get('input[name="password"]').type('wrongpassword');
cy.get('button[type="submit"]').click();
cy.contains('Invalid credentials').should('be.visible');
cy.url().should('include', '/login');
});
});
// Custom commands
// cypress/support/commands.ts
Cypress.Commands.add('login', (email, password) => {
cy.session([email, password], () => {
cy.visit('/login');
cy.get('input[name="email"]').type(email);
cy.get('input[name="password"]').type(password);
cy.get('button[type="submit"]').click();
cy.url().should('include', '/dashboard');
});
});
// Usage
cy.login('john@example.com', 'password123');
// Intercept network requests
cy.intercept('GET', '/api/users', {
statusCode: 200,
body: [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Smith' }
]
}).as('getUsers');
cy.visit('/users');
cy.wait('@getUsers');
cy.contains('John Doe').should('be.visible');
// Playwright
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Login Flow', () => {
test('allows user to login successfully', async ({ page }) => {
await page.goto('/login');
await page.fill('input[name="email"]', 'john@example.com');
await page.fill('input[name="password"]', 'password123');
await page.click('button[type="submit"]');
// Assertions
await expect(page).toHaveURL(/.*dashboard/);
await expect(page.locator('text=Welcome, John')).toBeVisible();
});
test('shows error for invalid credentials', async ({ page }) => {
await page.goto('/login');
await page.fill('input[name="email"]', 'invalid@example.com');
await page.fill('input[name="password"]', 'wrongpassword');
await page.click('button[type="submit"]');
await expect(page.locator('text=Invalid credentials')).toBeVisible();
await expect(page).toHaveURL(/.*login/);
});
});
// Playwright fixtures (reusable context)
// tests/fixtures.ts
import { test as base } from '@playwright/test';
type MyFixtures = {
authenticatedPage: Page;
};
export const test = base.extend<MyFixtures>({
authenticatedPage: async ({ page }, use) => {
await page.goto('/login');
await page.fill('input[name="email"]', 'john@example.com');
await page.fill('input[name="password"]', 'password123');
await page.click('button[type="submit"]');
await page.waitForURL('**/dashboard');
await use(page);
}
});
// Usage
test('user can access protected page', async ({ authenticatedPage }) => {
await authenticatedPage.goto('/profile');
await expect(authenticatedPage).toHaveURL(/.*profile/);
});
// Multi-browser testing (Playwright)
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
{ name: 'mobile-chrome', use: { ...devices['Pixel 5'] } },
{ name: 'mobile-safari', use: { ...devices['iPhone 12'] } }
]
});
// API mocking (Playwright)
await page.route('**/api/users', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 1, name: 'John Doe' }
])
});
});
// Screenshots and videos
test('takes screenshot on failure', async ({ page }) => {
await page.goto('/');
await page.screenshot({ path: 'screenshot.png' });
});
// Playwright traces (debugging)
npx playwright test --trace on
npx playwright show-trace trace.zip
// Parallel execution
// Cypress
npx cypress run --parallel --record --key YOUR_KEY
// Playwright
npx playwright test --workers 4
// Visual regression (Playwright)
await expect(page).toHaveScreenshot('homepage.png');
// Component testing (Cypress)
// components/Button.cy.tsx
import { Button } from './Button';
describe('Button Component', () => {
it('renders', () => {
cy.mount(<Button>Click me</Button>);
cy.contains('Click me').should('be.visible');
});
it('calls onClick', () => {
const onClick = cy.stub();
cy.mount(<Button onClick={onClick}>Click me</Button>);
cy.contains('Click me').click();
cy.wrap(onClick).should('have.been.called');
});
});
Modern Recommendation: Use Playwright for new projects (faster,
better browser support). Use Cypress for better DX and time-travel debugging.
4. Storybook Visual Regression Testing
| Tool | Method | Pricing | Use Case |
|---|---|---|---|
| Chromatic | Cloud-based visual diff, Storybook integration | Free tier + paid | Official Storybook solution, automated visual testing |
| Percy | Visual snapshots, cross-browser testing | Free tier + paid | Multi-platform, CI/CD integration |
| BackstopJS | Screenshot comparison, local testing | Free (open source) | Self-hosted, budget-friendly |
| Playwright Visual | Built-in screenshot comparison | Free | Integrated with Playwright E2E tests |
Example: Visual Regression Testing
// Chromatic with Storybook
// Install
npm install --save-dev chromatic
// Run visual tests
npx chromatic --project-token=YOUR_TOKEN
// package.json
{
"scripts": {
"chromatic": "chromatic --exit-zero-on-changes"
}
}
// GitHub Actions integration
// .github/workflows/chromatic.yml
name: Chromatic
on: push
jobs:
chromatic:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-node@v3
- run: npm ci
- run: npm run build-storybook
- uses: chromaui/action@v1
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
// Interaction testing with Storybook
// Button.stories.tsx
import { within, userEvent, expect } from '@storybook/test';
export const ClickInteraction: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const button = canvas.getByRole('button');
// Take screenshot before click
await expect(button).toBeInTheDocument();
// Interact
await userEvent.click(button);
// Verify state change
await expect(button).toHaveClass('active');
}
};
// Percy with Playwright
// Install
npm install --save-dev @percy/cli @percy/playwright
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
baseURL: 'http://localhost:3000'
}
});
// Visual test
import percySnapshot from '@percy/playwright';
test('homepage visual test', async ({ page }) => {
await page.goto('/');
await percySnapshot(page, 'Homepage');
});
test('responsive visual test', async ({ page }) => {
await page.goto('/');
// Desktop
await page.setViewportSize({ width: 1920, height: 1080 });
await percySnapshot(page, 'Homepage Desktop');
// Tablet
await page.setViewportSize({ width: 768, height: 1024 });
await percySnapshot(page, 'Homepage Tablet');
// Mobile
await page.setViewportSize({ width: 375, height: 667 });
await percySnapshot(page, 'Homepage Mobile');
});
// BackstopJS configuration
// backstop.json
{
"id": "my_project",
"viewports": [
{
"label": "phone",
"width": 375,
"height": 667
},
{
"label": "tablet",
"width": 768,
"height": 1024
},
{
"label": "desktop",
"width": 1920,
"height": 1080
}
],
"scenarios": [
{
"label": "Homepage",
"url": "http://localhost:3000",
"delay": 500
},
{
"label": "Login Page",
"url": "http://localhost:3000/login",
"delay": 500
}
],
"paths": {
"bitmaps_reference": "backstop_data/bitmaps_reference",
"bitmaps_test": "backstop_data/bitmaps_test",
"html_report": "backstop_data/html_report"
}
}
// Run BackstopJS
backstop reference // Create reference screenshots
backstop test // Compare against reference
backstop approve // Approve changes
// Playwright built-in visual regression
test('visual regression test', async ({ page }) => {
await page.goto('/');
// First run creates baseline
// Subsequent runs compare against baseline
await expect(page).toHaveScreenshot('homepage.png');
});
// Custom threshold
await expect(page).toHaveScreenshot('homepage.png', {
maxDiffPixels: 100,
threshold: 0.2
});
// Ignore specific elements
await expect(page).toHaveScreenshot('homepage.png', {
mask: [page.locator('.dynamic-content')]
});
// Component-level visual testing
test('button variants visual test', async ({ page }) => {
await page.goto('/storybook');
const variants = ['primary', 'secondary', 'danger'];
for (const variant of variants) {
await page.goto(`/storybook/button-${variant}`);
await expect(page.locator('.button')).toHaveScreenshot(`button-${variant}.png`);
}
});
// Storybook test runner with visual testing
// .storybook/test-runner.ts
import { toMatchImageSnapshot } from 'jest-image-snapshot';
expect.extend({ toMatchImageSnapshot });
module.exports = {
async postRender(page, context) {
const image = await page.screenshot();
expect(image).toMatchImageSnapshot({
customSnapshotsDir: `__snapshots__/${context.id}`,
customSnapshotIdentifier: context.id
});
}
};
// Run Storybook test runner
npm run test-storybook
ROI: Visual regression testing caught 40% more bugs than manual
testing in production systems (Shopify case study).
5. MSW API Mocking Integration Tests
| Tool | Approach | Environment | Use Case |
|---|---|---|---|
| MSW BEST | Service Worker intercepts, same API mocks for tests/dev | Browser + Node | Modern API mocking, integration tests, dev environment |
| json-server | Full REST API from JSON file | Node server | Quick prototypes, fake backend |
| Mirage JS | In-memory API server, ORM-like | Browser | Complex data relationships, Ember/React |
| Nock | HTTP request mocking for Node.js | Node only | Node.js API testing, backend tests |
Example: MSW API Mocking
// Installation
npm install msw --save-dev
// Define handlers
// src/mocks/handlers.ts
import { http, HttpResponse } from 'msw';
export const handlers = [
// GET request
http.get('/api/users', () => {
return HttpResponse.json([
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' }
]);
}),
// POST request
http.post('/api/users', async ({ request }) => {
const newUser = await request.json();
return HttpResponse.json(
{ id: 3, ...newUser },
{ status: 201 }
);
}),
// Dynamic route params
http.get('/api/users/:userId', ({ params }) => {
const { userId } = params;
return HttpResponse.json({
id: userId,
name: 'John Doe',
email: 'john@example.com'
});
}),
// Error response
http.get('/api/error', () => {
return HttpResponse.json(
{ message: 'Internal Server Error' },
{ status: 500 }
);
}),
// Delayed response
http.get('/api/slow', async () => {
await delay(2000);
return HttpResponse.json({ data: 'Slow response' });
})
];
// Setup for browser (development)
// src/mocks/browser.ts
import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';
export const worker = setupWorker(...handlers);
// Start in browser
// src/index.tsx
if (process.env.NODE_ENV === 'development') {
const { worker } = await import('./mocks/browser');
await worker.start();
}
// Setup for Node (testing)
// src/mocks/server.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
// Jest setup
// jest.setup.ts
import { server } from './mocks/server';
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
// Integration test with MSW
// UserList.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import { UserList } from './UserList';
test('displays list of users', async () => {
render(<UserList />);
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('Jane Smith')).toBeInTheDocument();
});
});
// Override handler for specific test
test('handles API error', async () => {
server.use(
http.get('/api/users', () => {
return HttpResponse.json(
{ message: 'Server error' },
{ status: 500 }
);
})
);
render(<UserList />);
await waitFor(() => {
expect(screen.getByText(/error loading users/i)).toBeInTheDocument();
});
});
// GraphQL mocking
import { graphql, HttpResponse } from 'msw';
const handlers = [
graphql.query('GetUser', ({ query, variables }) => {
return HttpResponse.json({
data: {
user: {
id: variables.id,
name: 'John Doe'
}
}
});
}),
graphql.mutation('CreateUser', ({ query, variables }) => {
return HttpResponse.json({
data: {
createUser: {
id: '123',
name: variables.name
}
}
});
})
];
// Request inspection
http.post('/api/users', async ({ request }) => {
const body = await request.json();
const headers = request.headers;
console.log('Request body:', body);
console.log('Auth header:', headers.get('Authorization'));
return HttpResponse.json({ id: 1, ...body });
});
// Conditional responses
http.get('/api/users', ({ request }) => {
const url = new URL(request.url);
const role = url.searchParams.get('role');
if (role === 'admin') {
return HttpResponse.json([
{ id: 1, name: 'Admin User', role: 'admin' }
]);
}
return HttpResponse.json([
{ id: 2, name: 'Regular User', role: 'user' }
]);
});
// Response composition
import { HttpResponse } from 'msw';
http.get('/api/users', () => {
return HttpResponse.json(
{ users: [] },
{
status: 200,
headers: {
'Content-Type': 'application/json',
'X-Custom-Header': 'value'
}
}
);
});
// Life-cycle events
server.events.on('request:start', ({ request }) => {
console.log('Outgoing:', request.method, request.url);
});
server.events.on('response:mocked', ({ response }) => {
console.log('Mocked:', response.status);
});
// Browser DevTools integration
// Enable in-browser debugging
worker.start({
onUnhandledRequest: 'warn' // or 'error', 'bypass'
});
// json-server (quick prototyping)
// db.json
{
"users": [
{ "id": 1, "name": "John Doe" },
{ "id": 2, "name": "Jane Smith" }
],
"posts": [
{ "id": 1, "userId": 1, "title": "Hello World" }
]
}
// Start server
npx json-server --watch db.json --port 3001
// Full CRUD API automatically available:
// GET /users
// GET /users/1
// POST /users
// PUT /users/1
// PATCH /users/1
// DELETE /users/1
Industry Standard: MSW is used by Microsoft, Netflix, Amazon.
Over 1M+ downloads/month. Winner of "Best Testing Tool" at JSNation 2022.
6. Coverage.js Istanbul Test Coverage
| Tool | Method | Integration | Use Case |
|---|---|---|---|
| Istanbul (nyc) | Code instrumentation, coverage reports | Jest, Mocha, CLI | Industry standard, comprehensive reports |
| c8 | Native V8 coverage, faster | Node.js, CLI | Modern Node.js, no instrumentation |
| Vitest Coverage | V8 or Istanbul | Vitest built-in | Vite projects, fast coverage |
| Jest Coverage | Istanbul integration | Jest built-in | React projects, zero config |
Example: Test Coverage Configuration
// Jest with coverage
// jest.config.js
module.exports = {
collectCoverage: true,
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.d.ts',
'!src/**/*.stories.tsx',
'!src/**/*.test.tsx',
'!src/index.tsx'
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
},
'./src/components/': {
branches: 90,
functions: 90,
lines: 90,
statements: 90
}
},
coverageReporters: ['text', 'lcov', 'html', 'json-summary']
};
// Run with coverage
npm test -- --coverage
npm test -- --coverage --watchAll=false
// Vitest with coverage
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
coverage: {
provider: 'v8', // or 'istanbul'
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'src/**/*.test.ts',
'src/**/*.spec.ts',
'**/*.config.ts'
],
thresholds: {
lines: 80,
functions: 80,
branches: 80,
statements: 80
}
}
}
});
// Run with coverage
npm run test -- --coverage
// NYC (Istanbul CLI)
// .nycrc
{
"all": true,
"include": ["src/**/*.js"],
"exclude": [
"**/*.test.js",
"**/node_modules/**"
],
"reporter": ["html", "text", "lcov"],
"check-coverage": true,
"lines": 80,
"functions": 80,
"branches": 80,
"statements": 80
}
// Run with nyc
npx nyc mocha
// Coverage reports interpretation
// Text output
------------|---------|----------|---------|---------|
File | % Stmts | % Branch | % Funcs | % Lines |
------------|---------|----------|---------|---------|
All files | 85.5 | 78.3 | 82.1 | 85.2 |
Button.tsx | 100 | 100 | 100 | 100 |
Input.tsx | 92.3 | 85.7 | 90 | 92.1 |
Modal.tsx | 68.4 | 50 | 60 | 67.8 |
------------|---------|----------|---------|---------|
// Coverage metrics explained:
// - Statements: % of executable statements covered
// - Branches: % of if/else branches covered
// - Functions: % of functions called
// - Lines: % of lines executed
// Ignore coverage for specific code
/* istanbul ignore next */
function debugOnly() {
console.log('Debug info');
}
// Ignore entire file
/* istanbul ignore file */
// c8 (native V8 coverage)
{
"scripts": {
"test:coverage": "c8 --reporter=html --reporter=text npm test"
}
}
// Codecov integration (CI/CD)
// .github/workflows/test.yml
name: Test Coverage
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm ci
- run: npm test -- --coverage
- uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
flags: unittests
name: codecov-umbrella
// Coveralls integration
- run: npm test -- --coverage --coverageReporters=text-lcov | coveralls
// SonarQube integration
sonar.javascript.lcov.reportPaths=coverage/lcov.info
// Custom coverage reporter
// custom-reporter.js
class CustomReporter {
onRunComplete(contexts, results) {
const { numTotalTests, numPassedTests } = results;
const coverage = (numPassedTests / numTotalTests) * 100;
console.log(`Coverage: ${coverage.toFixed(2)}%`);
if (coverage < 80) {
throw new Error('Coverage threshold not met');
}
}
}
module.exports = CustomReporter;
// Coverage badges (README.md)

// Branch-specific thresholds
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
},
'./src/critical/**/*.ts': {
branches: 100,
functions: 100,
lines: 100,
statements: 100
}
}
// Coverage diff in PRs
npx coverage-diff --base=main --head=feature-branch
// Enforce coverage on commit
// .husky/pre-push
#!/bin/sh
npm test -- --coverage --coverageThreshold='{"global":{"branches":80}}'
Frontend Testing Stack Summary
- Unit Tests: Vitest (5-10x faster than Jest) for Vite projects, Jest for React/CRA
- Component Tests: React Testing Library (test behavior not implementation)
- E2E Tests: Playwright (faster, better browsers) or Cypress (better DX)
- Visual Tests: Chromatic for Storybook, Playwright for E2E visual regression
- API Mocking: MSW (Mock Service Worker) - same mocks for dev and tests
- Coverage: Built-in with Jest/Vitest, aim for 80%+ with focus on branches
Testing Pyramid: Write mostly unit tests (70%), some integration tests (20%), few E2E tests (10%). E2E
tests are slow and brittle.