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

Jest vs Vitest

Feature Jest Vitest
Speed Good 5-10x faster
ESM Support Experimental Native
Config Separate Shares with Vite
HMR
Ecosystem Mature Growing

Testing Best Practices

  • ✅ Write tests before fixing bugs
  • ✅ Test behavior, not implementation
  • ✅ Use descriptive test names
  • ✅ Follow AAA: Arrange, Act, Assert
  • ✅ Mock external dependencies
  • ✅ Aim for 80%+ code coverage
  • ✅ Keep tests fast (<100ms each)
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');
});

RTL Query Methods

Query Returns Throws
getBy Element ✅ If not found
queryBy Element or null ❌ Never
findBy Promise<Element> ✅ If not found (async)
getAllBy Element[] ✅ If none found

Testing Checklist

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');
  });
});

Cypress vs Playwright

Feature Cypress Playwright
Browser Support Chrome, Firefox, Edge Chrome, Firefox, Safari
Speed Good Faster (parallel)
Debugging Excellent (time-travel) Good (traces)
Mobile Viewport only Full emulation
Learning Curve Easy Moderate

E2E Testing Best Practices

  • ✅ Test critical user journeys
  • ✅ Use data-testid for stability
  • ✅ Mock external APIs
  • ✅ Run in CI/CD pipeline
  • ✅ Take screenshots on failure
  • ✅ Keep tests independent
  • ✅ Use page objects pattern
  • ⚠️ E2E tests are slow (run critical paths)
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

Visual Testing Benefits

  • ✅ Catch unintended UI changes
  • ✅ Detect CSS regression
  • ✅ Verify responsive layouts
  • ✅ Cross-browser consistency
  • ✅ Component library stability
  • ✅ Automated design QA
  • ✅ Reduce manual testing

Visual Testing Checklist

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

MSW Benefits

  • ✅ Same mocks for tests and dev
  • ✅ No code changes needed
  • ✅ Intercepts at network level
  • ✅ Works with any HTTP client
  • ✅ GraphQL support
  • ✅ Browser and Node.js
  • ✅ TypeScript-first
  • ✅ No proxy server needed

API Mocking Use Cases

  • Development: Work without backend
  • Testing: Predictable API responses
  • E2E: Mock external services
  • Edge Cases: Test error scenarios
  • Offline: Work without internet
  • Performance: Fast local responses
  • Demos: Reliable demo data
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)
![Coverage](https://img.shields.io/codecov/c/github/username/repo)

// 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}}'

Coverage Metrics

Metric Target Priority
Statements 80%+ High
Branches 80%+ Critical
Functions 80%+ High
Lines 80%+ Medium

Coverage Best Practices

  • ✅ Aim for 80%+ coverage
  • ✅ 100% for critical paths
  • ✅ Focus on branch coverage
  • ✅ Exclude test files
  • ✅ Run in CI/CD pipeline
  • ✅ Track coverage trends
  • ⚠️ Don't chase 100% blindly
  • ⚠️ Quality > Quantity

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.