Frontend Testing Guide: Jest, Playwright, and Testing Library

Frontend Testing Guide: Jest, Playwright, and Testing Library — the complete guide for 2026.

</> DEPLOY
#1
Language 13 years running
100ms
Target interaction latency
4B
Browser users worldwide
95%
Sites using JavaScript

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.

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.

01

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).

02

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.

Code Example
Code
// 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.

03

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.

Code Example
Code
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.

04

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.

Code Example
Code
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.

05

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:

Code Example
Code
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.

06

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. June–October 2026 (Thu–Fri). Seats are limited.

Reserve Your Seat

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

The Bottom Line
The technology is ready. The tools are accessible. The only question is whether you will build something real with them. Every skill in this guide exists to help you ship work that matters.

Learn This. Build With It. Ship It.

The Precision AI Academy 2-day in-person bootcamp. Denver, NYC, Dallas, LA, Chicago. $1,490. June–October 2026 (Thu–Fri). 40 seats max.

Reserve Your Seat →
PA
Our Take

End-to-end tests are oversold. Unit tests are undersold. Integration tests are ignored.

The testing pyramid exists for a reason, and most frontend testing guidance reverses it in practice. End-to-end tests with Playwright or Cypress are compelling in demos — they run in a real browser and test the whole stack — but they are slow (minutes per suite), brittle (one DOM change breaks them), and provide no signal about where a failure occurred. Unit tests with Jest are fast, precise, and maintainable, but they test in isolation and miss integration failures. The middle tier — integration tests that render a component with real (or near-real) dependencies but do not spin up a browser — is underutilized and often the most valuable.

React Testing Library's philosophy (test behavior, not implementation) is correct but frequently misapplied. The most common misuse is testing component internals — checking whether a specific function was called, whether a particular state value is set — rather than testing what the user experiences. A test that breaks when you refactor a component's internal state management but the UI behavior is unchanged is a test that punishes good engineering. The rule of thumb: if your test would not break when a user has a problem, it is probably testing the wrong thing.

One practical recommendation for teams with no existing test coverage: do not start with unit tests. Start with three to five integration tests that cover your critical user paths (login, checkout, core workflow) using React Testing Library or Playwright with locators. Those high-value tests catch regressions, build team confidence in the process, and create a foundation for expanding coverage without debating the theory of testing.

PA

Published By

Precision AI Academy

Practitioner-focused AI education · 2-day in-person bootcamp in 5 U.S. cities

Precision AI Academy publishes deep-dives on applied AI engineering for working professionals. Founded by Bo Peng (Kaggle Top 200) who leads the in-person bootcamp in Denver, NYC, Dallas, LA, and Chicago.

Kaggle Top 200 Federal AI Practitioner 5 U.S. Cities Thu–Fri Cohorts