This episode teaches unit testing with Jest and React Testing Library, snapshot testing, integration testing for component behavior, and E2E testing with Cypress or Playwright. Code quality is guaranteed, not merely hoped for.

Untested code turns into dreaded code. Episode 16 makes testing a natural part of your React workflow, not an obligation to keep postponing.
We build the testing pyramid from the bottom up: unit testing with Jest and React Testing Library, snapshot testing, integration testing for behavior across components, and E2E testing with Cypress or Playwright for real user flows in the browser.
For a Vite project, Vitest is the natural match because it uses the same Vite config. Install it and add the scripts:
npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom{
"scripts": {
"test": "vitest",
"test:run": "vitest run"
}
}npm install -D vitest ... installs the test framework along with Testing Library. npm run test runs vitest in watch mode; vitest run runs once in CI.
React Testing Library tests behavior from the user's perspective — render the component, interact, and assert the results:
import { render, screen, fireEvent } from "@testing-library/react"
import { describe, it, expect } from "vitest"
import Counter from "./Counter.jsx"
describe("Counter", () => {
it("menambah angka saat tombol diklik", () => {
render(<Counter />)
const tombol = screen.getByRole("button")
fireEvent.click(tombol)
expect(screen.getByText("1")).toBeTruthy()
})
})screen.getByRole("button") finds the element by its accessibility role, not by text or class — mimicking how users and screen readers find elements. fireEvent.click simulates a click, then expect checks the result.
Snapshot testing stores a serialized output of the component and compares it on the next test run. Writing one is as easy as a single line:
it("konsisten dengan snapshot", () => {
const { container } = render(<Profil nama="Arman" />)
expect(container).toMatchSnapshot()
})expect(container).toMatchSnapshot() creates a snapshot file on first run, then compares against it on every run. Snapshots are great for detecting unintentional markup changes — for example, a CSS class that shifted.
Snapshots easily produce false positives: insignificant small changes fail the test, and developers get used to updating snapshots without looking. Use snapshots for stable components, and combine them with behavior assertions — not replace them.
An integration test combines several components and tests their flow together. Example: a login form that calls a handler and shows a message:
import { render, screen, fireEvent, waitFor } from "@testing-library/react"
it("menampilkan error saat login gagal", async () => {
render(<FormLogin />)
fireEvent.change(screen.getByLabelText(/email/i), {
target: { value: "salah@contoh.com" },
})
fireEvent.click(screen.getByRole("button", { name: /masuk/i }))
await waitFor(() => {
expect(screen.getByText(/kredensial salah/i)).toBeTruthy()
})
})fireEvent.change fills the input, the click triggers the submit, then waitFor waits for the async result. This integration test ensures the form, validation, and handler work as a unit.
Query priority in Testing Library: getByRole and getByLabelText are better than getByTestId. Role-based queries force components to have good accessibility, and the tests aren't brittle to class or markup changes.
Unit and integration tests don't guarantee the whole flow works. E2E runs the real app in a browser. Playwright is the modern choice:
npm init playwright@latestimport { test, expect } from "@playwright/test"
test("login lalu melihat dashboard", async ({ page }) => {
await page.goto("http://localhost:5173/login")
await page.fill("#email", "admin@contoh.com")
await page.fill("#password", "rahasia123")
await page.click("button:has-text('Masuk')")
await expect(page).toHaveURL(/dashboard/)
})page.goto opens the real app, page.fill fills the form, and expect(page).toHaveURL ensures navigation succeeded. E2E tests what unit tests can't reach: routing, real rendering, and cross-page interactions.
Keep the pyramid right: many fast unit tests, some integration tests, and a few slow but most convincing E2E tests. Run E2E in CI, not on every save.
Tip
Start E2E with the most critical flows: login, checkout, and registration. These flows break most often and are most expensive if they fail in production.
Episode 16 built a culture of quality: unit tests with Vitest and React Testing Library, snapshot testing for markup regression detection, integration testing for behavior across components, and E2E with Playwright for real flows in the browser.
Key takeaways:
getByRole, getByLabelText.In the next episode, episode 17, we'll cover accessibility & UX — ARIA roles, keyboard navigation, and focus management, semantic HTML and accessible forms, mobile-first responsive design, and the basics of internationalization. Your app will be open to everyone.