---
title: "13 — Testing React — RTL, Jest, Vitest, Playwright, and the Pyramid"
uid: testing-react
tags: ["vitest", "jest", "react", "playwright", "roadmap:react", "testing", "cypress", "react-testing-library"]
excerpt: "Pick the tool by what you're testing: Vitest/Jest for unit logic, React Testing Library for component behavior, Playwright/Cypress for end-to-end. Each layer does a job the others can't."
date: 2026-08-13T03:27:44+0000
source: https://www.aveshina.my.id/en/blog/testing-react
---

A pile of tool names was how React testing felt until I matched each one to a rung on the pyramid. The framing that organized it: **pick the tool by what you're testing, and match it to the testing pyramid.** [1] Unit logic runs in Vitest or Jest. Component behavior runs in React Testing Library. Full user flows through a real browser run in Playwright or Cypress. Each layer does a job the others can't, and pretending one tool covers everything is how test suites get slow, brittle, or both.

## The testing pyramid, briefly

The pyramid is the way of thinking: a wide base of fast, cheap **unit tests**, a middle layer of **integration/component tests**, and a small apex of slow, expensive **end-to-end tests** [1]. The economics are the point — unit tests run in milliseconds and give precise failure pointers; E2E tests run in seconds, exercise the whole stack, but flake more and pin down less when they fail. A healthy suite is mostly the bottom, some of the middle, and a few of the top.

## The unit layer: Vitest and Jest

For testing pure logic — a reducer, a utility function, a custom hook's internals — a JavaScript test runner is the tool. The roadmap lists both **Vitest** and **Jest** [2][3].

- **Jest** is the established framework — simple, works with Babel/TS/React, matcher-rich, and the historical default for React projects [3].
- **Vitest** is the Vite-native successor — fast, ESM/TS/JSX out of the box, and a Jest-compatible API so the migration is small [2].

The choice tracks the build tool: if the project is on Vite (which it should be — see the tooling notes), Vitest is the natural fit and shares the same config. For legacy create-react-app codebases, Jest is what's there. Both run isolated unit tests against pure functions, and both can render components with React Testing Library.

## The component layer: React Testing Library

React Testing Library (RTL) is the standard for testing component **behavior** [4]. Its guiding principle flipped how I write component tests: **"the more your tests resemble the way your software is used, the more confidence they can give you."** [4] In practice, that means RTL renders the component into a real DOM and lets me interact with it the way a user does — finding elements by their visible text or label, clicking buttons, asserting on what's rendered — rather than testing the component's internal implementation.

```
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

test('submits the form', async () => {
  render(<SignupForm />);
  await userEvent.type(screen.getByLabelText(/email/i), 'a@b.com');
  await userEvent.click(screen.getByRole('button', { name: /submit/i }));
  expect(await screen.findByText(/welcome/i)).toBeInTheDocument();
});
```

The deliberate constraint is that RTL **doesn't expose component internals** — no instance(), no direct state access. If I can't test something through the rendered output, the test is telling me the component's API is wrong. This is what makes RTL tests survive refactors: I can rewrite the component's internals entirely, and a behavior-based test still passes because the user-visible behavior didn't change.

## The end-to-end layer: Playwright and Cypress

For testing full user flows through a real browser — load the app, sign in, fill a form, verify the database updated — an E2E framework is the tool. The roadmap lists both **Playwright** and **Cypress** [5][6].

- **Playwright** (Microsoft) drives Chromium, WebKit, and Firefox via the DevTools protocol, runs on all OSes, and is built for stable, parallelizable E2E tests [5]. It can see into and control the browser directly, which makes for reliable tests that simulate real user scenarios. It's the modern default for E2E.
- **Cypress** is the older incumbent — runs in the browser, has a polished interactive UI for watching tests run, and is well-loved for developer experience [6]. Its architectural choices (in-browser execution, historically single-browser) are the tradeoffs Playwright reacts to.

E2E tests are the apex of the pyramid precisely because they're expensive: they spin up a browser, often a test database, sometimes the whole backend. A suite of hundreds of E2E tests becomes slow and flaky. The discipline is to cover the critical user journeys (sign-in, checkout, the core flow) with E2E, and push everything else down to the component and unit layers.

```figure
<svg viewBox="0 0 600 340" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="Testing pyramid. A wide base layer for unit tests (Vitest, Jest). A middle layer for component/integration tests (React Testing Library). A small apex for end-to-end tests (Playwright, Cypress). Each layer labeled with what it tests and its speed/cost tradeoff.">
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">
    <!-- base -->
    <polygon points="60,300 540,300 460,210 140,210" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <text x="300" y="246" font-size="13" font-weight="700" fill="#052e16" text-anchor="middle">Unit — Vitest · Jest</text>
    <text x="300" y="266" font-size="10.5" fill="#052e16" text-anchor="middle">pure logic · reducers · utils</text>
    <text x="300" y="284" font-size="9.5" fill="#64748b" text-anchor="middle" font-style="italic">milliseconds · precise failure pointers</text>

    <!-- middle -->
    <polygon points="140,210 460,210 400,130 200,130" fill="#fef9c3" stroke="#ca8a04" stroke-width="1.5"/>
    <text x="300" y="166" font-size="13" font-weight="700" fill="#422006" text-anchor="middle">Component — React Testing Library</text>
    <text x="300" y="186" font-size="10.5" fill="#422006" text-anchor="middle">behavior via real DOM · user-style interaction</text>
    <text x="300" y="202" font-size="9.5" fill="#64748b" text-anchor="middle" font-style="italic">survives refactors · renders into jsdom</text>

    <!-- apex -->
    <polygon points="200,130 400,130 300,50" fill="#fee2e2" stroke="#dc2626" stroke-width="1.5"/>
    <text x="300" y="92" font-size="12" font-weight="700" fill="#7f1d1d" text-anchor="middle">E2E — Playwright · Cypress</text>
    <text x="300" y="108" font-size="10" fill="#7f1d1d" text-anchor="middle">full browser · critical journeys</text>
    <text x="300" y="122" font-size="9" fill="#64748b" text-anchor="middle" font-style="italic">slow · flake-prone · use sparingly</text>

    <!-- y axis -->
    <line x1="40" y1="300" x2="40" y2="50" stroke="#475569" stroke-width="1.5"/>
    <text x="28" y="180" font-size="10" fill="#475569" text-anchor="middle" font-style="italic" transform="rotate(-90 28 180)">cost · slowness · flake</text>
    <text x="28" y="200" font-size="10" fill="#475569" text-anchor="middle" font-style="italic" transform="rotate(-90 28 200)"></text>
  </g>
</svg>
```

## How I use this

The rule I run is "push tests down the pyramid wherever possible." Pure logic — reducers, selectors, utility functions — gets a Vitest unit test, because it's fast and pins the failure to one function. Component behavior gets a React Testing Library test that interacts through the rendered output, because behavior-based tests survive refactors and catch the bugs users actually hit. E2E tests cover only the critical user journeys — sign-in, the main flow, checkout — because a suite full of E2E is slow and brittle. The tools I default to: Vitest (because my projects are on Vite), React Testing Library for components, and Playwright for E2E. Jest and Cypress I maintain in legacy codebases but don't pick for new work. The discipline isn't about which tool is "best" in isolation — it's matching each test to the layer that runs it fastest while still giving real confidence.

## References

[1] BrowserStack, "Testing pyramid for test automation," browserstack.com, 2024. [Online]. Available: [https://www.browserstack.com/guide/testing-pyramid-for-test-automation](https://www.browserstack.com/guide/testing-pyramid-for-test-automation)

[2] Vitest, "Vitest — a Vite-native unit test framework," vitest.dev, 2024. [Online]. Available: [https://vitest.dev/](https://vitest.dev/)

[3] Jest, "Jest — delightful JavaScript testing," jestjs.io, 2024. [Online]. Available: [https://jestjs.io/](https://jestjs.io/)

[4] Testing Library, "React Testing Library — intro," testing-library.com, 2024. [Online]. Available: [https://testing-library.com/docs/react-testing-library/intro/](https://testing-library.com/docs/react-testing-library/intro/)

[5] Microsoft, "Playwright — fast and reliable end-to-end testing," playwright.dev, 2024. [Online]. Available: [https://playwright.dev/](https://playwright.dev/)

[6] Cypress, "Cypress — the web has evolved, testing should too," cypress.io, 2024. [Online]. Available: [https://www.cypress.io/](https://www.cypress.io/)

[7] R. Wieruch, "How to use React Testing Library," robinwieruch.de, 2023. [Online]. Available: [https://www.robinwieruch.de/react-testing-library/](https://www.robinwieruch.de/react-testing-library/)

```quiz
Q: The testing pyramid's central economic idea is:
- a few fast unit tests at the base, some component tests in the middle, a small number of slow E2E tests at the apex
- all tests should be E2E for maximum confidence
- unit tests are obsolete in React
correct: 0
explain: Unit tests are cheap and precise; E2E tests are expensive, slow, and flake-prone. A healthy suite is mostly the bottom, some of the middle, and a few at the top — push tests down wherever you can.

Q: React Testing Library's guiding principle is:
- test the component's internal implementation and state directly
- the more your tests resemble how the software is used, the more confidence they give — interact via visible output, not internals
- mock everything so tests run instantly
correct: 1
explain: RTL renders into a real DOM and interacts through accessible, user-visible queries. It deliberately hides internals, so tests survive refactors and catch bugs users actually hit.

Q: Vitest vs Jest today:
- Vitest is Vite-native (ESM/TS/JSX out of the box) and Jest-compatible; Jest is the legacy default for CRA-era projects
- Vitest only works with Angular
- Jest is faster than Vitest
correct: 0
explain: On a Vite project, Vitest shares config and is the natural fit with a Jest-compatible API. Jest remains in CRA/legacy codebases. Both can drive React Testing Library.

Q: Playwright's defining capabilities are:
- it drives Chromium, WebKit, and Firefox via the DevTools protocol for stable, cross-browser E2E tests
- it runs only inside the browser like Cypress
- it's a unit test runner
correct: 0
explain: Playwright controls the browser directly across engines and OSes, enabling reliable, parallelizable E2E tests. Cypress runs in-browser (historically single-browser), which is the tradeoff Playwright reacts to.

Q: A test that loads the app, signs in, and verifies the DB updated belongs in:
- the E2E layer (Playwright/Cypress) — a critical journey through the full stack
- a Vitest unit test
- an RTL component test with everything mocked
correct: 0
explain: Full-stack flows through a real browser are E2E. They're expensive, so reserve them for critical journeys and push everything else down to component and unit tests.
```
