Frontend Testing Guide: Jest, Playwright, and Testing Library

In This Guide

  1. The Testing Pyramid: Unit, Integration, E2E
  2. Unit Tests with Jest
  3. Component Tests with React Testing Library
  4. End-to-End Tests with Playwright
  5. Test Coverage: What to Aim For
  6. Testing in CI/CD
  7. Frequently Asked Questions

Key Takeaways

Most developers write too few tests until a production bug burns them badly enough to write more. Then they sometimes overcorrect and write so many tests that refactoring becomes painful because tests break on every change to implementation details.

Good testing is about finding the right level at each layer of the testing pyramid: enough unit tests to catch logic errors fast, enough integration tests to verify key workflows, and enough E2E tests to ensure critical user journeys work end-to-end — without making the test suite so slow and brittle that the team stops running it.

The Testing Pyramid: Unit, Integration, E2E

The testing pyramid describes the ideal distribution of test types: many fast unit tests at the base, fewer integration tests in the middle, and a small number of slow E2E tests at the top. The pyramid shape reflects the cost and speed trade-offs at each level.

Unit tests test individual functions and components in isolation. They mock all dependencies (APIs, databases, other modules). They run in milliseconds. A suite of 500 unit tests completes in under a minute.

Integration tests test the interaction between components or between the application and real dependencies (database, API). They test that the pieces work together, not just that they work in isolation. They are slower than unit tests but catch problems that unit tests with mocks miss.

End-to-end tests (E2E) simulate a real user interacting with the full application in a real browser. They catch the final category of bugs — UI interactions, full request/response cycles, multi-step workflows — but they are the slowest (seconds to minutes per test) and the most brittle (dependent on timing, element selectors, and environment state).

The recommended distribution for a typical web application: 70% unit tests, 20% integration tests, 10% E2E tests. More E2E tests for applications where the full user journey is the product (e-commerce checkout, onboarding flows).

Unit Tests with Jest

Jest is the dominant JavaScript/TypeScript test runner for unit and integration tests. It provides a test runner, assertion library, mock/spy utilities, and code coverage — all in one package.

// utils/format.test.ts
import { formatCurrency } from './format'

describe('formatCurrency', () => {
  it('formats a number as USD currency', () => {
    expect(formatCurrency(1234.5)).toBe('$1,234.50')
  })
  it('handles zero', () => {
    expect(formatCurrency(0)).toBe('$0.00')
  })
  it('handles negative values', () => {
    expect(formatCurrency(-50)).toBe('-$50.00')
  })
})

Jest features to know: jest.fn() creates mock functions, jest.spyOn() mocks specific methods while preserving the original, jest.mock('module') replaces an entire module import with a mock, and beforeEach/afterEach run setup and teardown before and after each test.

For async code, use async/await with Jest natively or use done callbacks for callback-based async. Always return or await Promises in tests — Jest will not catch assertion failures in unawaited async code.

Component Tests with React Testing Library

React Testing Library (RTL) tests React components the way users interact with them — through text, labels, roles, and visible UI — rather than testing internal component state or implementation details.

import { render, screen, fireEvent } from '@testing-library/react'
import { LoginForm } from './LoginForm'

test('submits form with email and password', async () => {
  const onSubmit = jest.fn()
  render(<LoginForm onSubmit={onSubmit} />)
  
  fireEvent.change(screen.getByLabelText('Email'), {
    target: { value: '[email protected]' }
  })
  fireEvent.change(screen.getByLabelText('Password'), {
    target: { value: 'password123' }
  })
  fireEvent.click(screen.getByRole('button', { name: 'Sign In' }))
  
  expect(onSubmit).toHaveBeenCalledWith({
    email: '[email protected]',
    password: 'password123'
  })
})

RTL querying priority (use the highest-priority query that applies): getByRole (buttons, inputs by role and accessible name), getByLabelText (form fields), getByText (visible text), getByTestId (last resort — adds test coupling to implementation). Using role-based queries ensures your tests also verify accessibility.

End-to-End Tests with Playwright

Playwright is Microsoft's E2E testing framework. It controls real browsers (Chromium, Firefox, WebKit) through a TypeScript/JavaScript API, supports parallel test execution, and includes trace viewer and screenshot-on-failure debugging capabilities.

import { test, expect } from '@playwright/test'

test('user can sign up and see dashboard', async ({ page }) => {
  await page.goto('https://myapp.com/signup')
  await page.fill('[name="email"]', '[email protected]')
  await page.fill('[name="password"]', 'SecurePass123!')
  await page.click('button[type="submit"]')
  await expect(page).toHaveURL(/\/dashboard/)
  await expect(page.getByText('Welcome')).toBeVisible()
})

Playwright features that make it better than Cypress for most new projects: auto-wait (Playwright automatically waits for elements to be actionable — no manual cy.wait() calls), multi-browser support in a single run, isolated browser contexts (test isolation without browser restarts), and first-class TypeScript support.

For E2E tests: focus on critical user journeys (sign up, sign in, core feature usage, checkout). Do not write E2E tests for every UI state — that is what RTL tests are for.

Testing in CI/CD

Your test suite is only as valuable as how often it runs. A test suite that only runs on a developer's laptop catches bugs too late. Run tests on every pull request and block merges that fail tests.

GitHub Actions test workflow:

name: Tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: {{ node-version: '20' }}
      - run: npm ci
      - run: npm test -- --coverage
      - run: npx playwright install --with-deps
      - run: npx playwright test

Optimize for speed: run unit tests first (fast, fail fast on logic errors), then integration tests, then E2E tests. Fail the pipeline immediately on unit test failure rather than running the full suite. Cache node_modules between runs. Run E2E tests only on PRs to the main branch if they are slow.

Frequently Asked Questions

How many tests should I write?

Write enough tests to give you confidence that the code works and enough to catch regressions when you change it. A practical target: write tests for every public function, every edge case that is non-obvious, every bug you find in production (a test that would have caught it), and every critical user journey. Test coverage of 70-80% is a reasonable target for most applications.

What is the difference between Jest and Vitest?

Both are JavaScript test runners. Jest is more mature with a larger ecosystem. Vitest is faster (uses Vite's bundler), has a nearly identical API to Jest, and is the preferred choice for new projects using Vite (which includes Nuxt, SvelteKit, and Vite-based React setups). For Next.js projects, Jest with SWC transformer is still the most common choice in 2026.

Should I use Playwright or Cypress for E2E tests?

For new projects in 2026, Playwright is the recommended choice. It has better multi-browser support, faster parallel execution, auto-waiting that eliminates most flakiness, and first-class TypeScript support. Cypress has a better developer experience for interactive debugging and a longer track record. If your team already has significant Cypress test infrastructure, there is no compelling reason to migrate.

What is code coverage and how do I measure it?

Code coverage measures the percentage of your code that is executed when your tests run. Jest generates coverage reports with the --coverage flag. Lines, branches, functions, and statements are the four coverage metrics. Branch coverage is the most meaningful — it ensures both the true and false paths of every conditional are tested. Configure Jest's coverageThreshold to fail the build if coverage drops below your minimum.

Well-tested code ships faster with fewer regressions. Get the skills.

Join professionals from Denver, NYC, Dallas, LA, and Chicago for two days of hands-on AI and tech training. $1,490. October 2026. Seats are limited.

Reserve Your Seat

Note: Information in this article reflects the state of the field as of early 2026.

BP

Bo Peng

AI Instructor & Founder, Precision AI Academy

Bo has trained 400+ professionals in applied AI across federal agencies and Fortune 500 companies. He founded Precision AI Academy to bridge the gap between AI theory and real-world professional application.