Choose the smallest test that can prove the behavior
Tests are most valuable when they fail for a product problem, not because an internal refactor changed a class name or helper call. Start with a plain unit test for isolated business logic, use a component test for a user-visible interaction, and reserve a browser test for the few journeys where routing, authentication, integrations, or deployment configuration genuinely matter.
Unit tests
Validate pure calculations, parsing, permissions, and state transitions with fast, focused inputs.
Component tests
Render a component, make an accessible query, and simulate the action a user would take.
Browser tests
Protect checkout, sign-in, creation, and other cross-page journeys with a small, reliable suite.
The goal is not an even percentage across test types. It is a deliberate mapping from risk to feedback speed. A pricing calculation needs many cheap cases; a payment redirect needs one or two end-to-end checks that prove the integration still works.
Test behavior through accessible names and outcomes
React Testing Library is effective because it encourages the same questions a user asks: can I find the control, understand its state, complete the task, and recover from an error? Prefer roles, labels, and visible text over selectors tied to markup structure. This makes tests more durable and catches many usability regressions along the way.
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import SaveProfileButton from "./SaveProfileButton";
it("shows confirmation after a successful save", async () => {
const user = userEvent.setup();
render(<SaveProfileButton onSave={() => Promise.resolve()} />);
await user.click(screen.getByRole("button", { name: "Save profile" }));
expect(await screen.findByText("Profile saved")).toBeInTheDocument();
});Avoid asserting that a specific hook ran or that a particular child received a private prop. Those details can change while the product behavior stays correct. When a control cannot be located by role and name, treat that as a useful accessibility signal rather than reaching immediately for a test-only attribute. Our React accessibility testing guide explains why labels, focus, and semantic controls are part of product quality.
Make data, time, and network responses deterministic
Flaky tests damage trust faster than missing tests. Create small fixtures that express the scenario at hand, reset mutable state between cases, and control values that naturally vary: the current time, generated IDs, locale, network latency, and third-party responses. A test should say what matters about a customer, order, or project instead of importing an enormous production-shaped object.
Mock external boundaries, not the entire application. For example, mock a payment provider response at the adapter layer while letting the order form, validation, and error UI run normally. This keeps the test realistic while avoiding network dependence. Keep authorization checks on the server as well; the React Server Components boundary guide covers how to keep trusted data and client interactions separate.
Use Playwright for the journeys that must not break
Browser tests are slower and broader, so they should be selective. Cover the paths that earn revenue, create irreversible data, or are especially difficult to simulate in isolation: signing in, completing onboarding, submitting a lead, checking out, or recovering from a failed save. Run them against a predictable seeded environment with test accounts that do not collide with parallel runs.
import { test, expect } from "@playwright/test";
test("a member can create a project", async ({ page }) => {
await page.goto("/projects");
await page.getByRole("button", { name: "New project" }).click();
await page.getByLabel("Project name").fill("Website refresh");
await page.getByRole("button", { name: "Create project" }).click();
await expect(page.getByRole("heading", { name: "Website refresh" })).toBeVisible();
});Assert the meaningful destination, not every pixel along the way. Include an unhappy path where it has high business value: an expired session, rejected card, denied permission, or retry after a transient error. Recovery behavior pairs naturally with the guidance in ourReact error-boundary recovery guide.
Design CI feedback people can act on
A healthy pipeline answers a reviewer question quickly: did this change break a known behavior? Run formatting, static checks, and targeted Vitest suites on each change. Run browser tests in parallel where the environment supports it, retain traces and screenshots for failures, and quarantine only after a flaky test has an owner and a repair plan.
- Keep fast component feedback short enough to run before a pull request is reviewed.
- Publish readable failure output, traces, and screenshots instead of only an exit code.
- Retry infrastructure failures carefully, but never hide a repeatable product failure.
- Review slow or flaky tests as production maintenance work, not background noise.
- Measure critical-path coverage by user journey, not a single global percentage.
React testing release checklist
✓ Each behavior has the smallest useful test layer
✓ Component tests use roles, labels, and user-visible outcomes
✓ Fixtures, time, IDs, and network responses are deterministic
✓ Mocks stop at external boundaries instead of replacing the application
✓ Browser tests cover only high-value cross-page journeys
✓ Test environments use isolated accounts and predictable seed data
✓ CI keeps logs, screenshots, or traces for failed browser checks
✓ Flaky tests are fixed or owned rather than silently retried forever
Ship React changes with dependable feedback
Endurance Softwares helps teams build and modernize React and Next.js products with practical testing, accessible interfaces, secure architecture, and reliable delivery practices.