V0 is now available! We'll release moreeee soooon .
Writing tests for React apps doesn't have to be complex. A modern strategy involves a combination of unit, integration, and end-to-end (E2E) testing — each serving a distinct purpose.
This post outlines a simple but effective approach to testing React apps in 2025 using tools like Vitest, Testing Library, and Playwright.
Use unit tests for pure functions, hooks, and small components without side effects.
Tool: vitest
Goal: Fast feedback on isolated logic
// hooks/useCounter.ts
export function useCounter() {
  const [count, setCount] = useState(0);
  const increment = () => setCount((c) => c + 1);
  return { count, increment };
}// hooks/useCounter.test.ts
import { renderHook, act } 
✅ Keep unit tests pure and isolated. Avoid DOM or network unless mocked.
Test UI + logic + interactions, but keep external dependencies mocked.
Tool: @testing-library/react + msw (for mocking API)
Goal: Ensure components render and behave correctly with real user interactions.
// components/UserList.tsx
export function UserList() {
  const { data } = useQuery(["users"], fetchUsers);
  return (
    <ul>
      {data?.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}// components/UserList.test.tsx
import { render, screen } from "@testing-library/react";
import { rest } from "msw";
import { setupServer } from "msw/node";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { UserList } from "./UserList";
 
const server = setupServer(
  rest.get("/api/users", (_, res, ctx) =>
    res(ctx.json([{ id: 1, name: "Alice" }]))
  )
);
 
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
 
test("renders fetched users", async () => {
  const queryClient = new QueryClient();
  render(
    <QueryClientProvider client={queryClient}>
      <UserList />
    </QueryClientProvider>
  );
  expect(await screen.findByText("Alice")).toBeInTheDocument();
});✅ Focus on behavior, not implementation details. Simulate user flows, not component internals.
E2E tests ensure everything works from a real user’s perspective: the app, APIs, routing, styles, etc.
Tool: playwright
Goal: Catch real-world issues before users do.
// e2e/home.spec.ts
import { test, expect } from "@playwright/test";
 
test("homepage has welcome text", async ({ page }) => {
  await page.goto("http://localhost:3000");
  await expect(page.getByText("Welcome")).toBeVisible();
});✅ Write fewer E2E tests (only critical paths). They are slower, but valuable.
| Layer | Tooling | Use case | 
|---|---|---|
| Unit | vitest | Pure functions, hooks | 
| Integration | Testing Library,msw | Components, user interactions | 
| E2E | playwright | Full app, routing, backend | 
A well-tested React app balances speed, reliability, and realism.
Don’t test everything — test what matters.
If a test gives you confidence your feature works and won’t break, it's a good test.
Happy testing 🔍