AiTechWorlds
AiTechWorlds
A test is code that verifies your component behaves correctly, so you catch a regression before a user does.
Think of it as a pre-flight checklist a pilot runs before every takeoff — not because the plane is expected to fail, but because catching a problem on the ground is infinitely cheaper than discovering it in the air. Tests give you the same confidence to refactor freely and document how your components are supposed to behave. This lesson covers the modern React testing stack: Vitest as the test runner and React Testing Library as the component testing tool.
React Testing Library has a guiding principle: test what users see and do, not implementation details. A user doesn't care which state variable is set — they care whether the button appears, whether clicking it shows a message, whether the form validates correctly.
// Wrong: Testing implementation (brittle — breaks on refactor)
expect(wrapper.state("isOpen")).toBe(true);
expect(component.find("DropdownMenu").props().isOpen).toBe(true);
// Correct: Testing behavior (resilient — focuses on what users experience)
expect(screen.getByRole("listbox")).toBeInTheDocument();npm install -D vitest @vitejs/plugin-react jsdom @testing-library/react @testing-library/user-event @testing-library/jest-dom// vitest.config.ts
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
test: {
environment: "jsdom",
globals: true,
setupFiles: "./src/test/setup.ts",
},
});// src/test/setup.ts
import "@testing-library/jest-dom";// package.json
{
"scripts": {
"test": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest --coverage"
}
}// src/components/Button.test.tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Button } from "./Button";
import { describe, it, expect, vi } from "vitest";
describe("Button", () => {
it("renders the label", () => {
render(<Button>Click me</Button>);
expect(screen.getByRole("button", { name: "Click me" })).toBeInTheDocument();
});
it("calls onClick when clicked", async () => {
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Submit</Button>);
await userEvent.click(screen.getByRole("button"));
expect(handleClick).toHaveBeenCalledOnce();
});
it("is disabled when disabled prop is true", () => {
render(<Button disabled>Save</Button>);
expect(screen.getByRole("button")).toBeDisabled();
});
it("shows loading state", () => {
render(<Button loading>Submit</Button>);
expect(screen.getByText("Loading...")).toBeInTheDocument();
expect(screen.getByRole("button")).toBeDisabled();
});
});// src/components/LoginForm.test.tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { LoginForm } from "./LoginForm";
import { vi } from "vitest";
describe("LoginForm", () => {
it("shows validation errors for empty fields", async () => {
render(<LoginForm onSubmit={vi.fn()} />);
await userEvent.click(screen.getByRole("button", { name: "Sign in" }));
expect(screen.getByText("Email is required")).toBeInTheDocument();
expect(screen.getByText("Password is required")).toBeInTheDocument();
});
it("shows error for invalid email", async () => {
render(<LoginForm onSubmit={vi.fn()} />);
await userEvent.type(screen.getByLabelText("Email"), "not-an-email");
await userEvent.click(screen.getByRole("button", { name: "Sign in" }));
expect(screen.getByText("Valid email required")).toBeInTheDocument();
});
it("calls onSubmit with credentials", async () => {
const handleSubmit = vi.fn();
render(<LoginForm onSubmit={handleSubmit} />);
await userEvent.type(screen.getByLabelText("Email"), "alice@example.com");
await userEvent.type(screen.getByLabelText("Password"), "password123");
await userEvent.click(screen.getByRole("button", { name: "Sign in" }));
expect(handleSubmit).toHaveBeenCalledWith({
email: "alice@example.com",
password: "password123",
});
});
it("disables submit button while submitting", async () => {
const handleSubmit = vi.fn(() => new Promise(resolve => setTimeout(resolve, 100)));
render(<LoginForm onSubmit={handleSubmit} />);
await userEvent.type(screen.getByLabelText("Email"), "alice@example.com");
await userEvent.type(screen.getByLabelText("Password"), "password123");
await userEvent.click(screen.getByRole("button", { name: "Sign in" }));
expect(screen.getByRole("button", { name: /Signing in/i })).toBeDisabled();
});
});// src/components/UserProfile.test.tsx
import { render, screen, waitFor } from "@testing-library/react";
import { UserProfile } from "./UserProfile";
import { vi } from "vitest";
// Mock fetch
vi.mock("@/lib/api", () => ({
fetchUser: vi.fn(),
}));
import { fetchUser } from "@/lib/api";
describe("UserProfile", () => {
it("shows loading state initially", () => {
vi.mocked(fetchUser).mockResolvedValue({ name: "Alice", email: "alice@example.com" });
render(<UserProfile userId="1" />);
expect(screen.getByRole("status", { name: "Loading" })).toBeInTheDocument();
});
it("displays user data after loading", async () => {
vi.mocked(fetchUser).mockResolvedValue({ name: "Alice", email: "alice@example.com" });
render(<UserProfile userId="1" />);
await screen.findByText("Alice"); // findBy* waits for element to appear
expect(screen.getByText("alice@example.com")).toBeInTheDocument();
});
it("shows error message when fetch fails", async () => {
vi.mocked(fetchUser).mockRejectedValue(new Error("Not found"));
render(<UserProfile userId="1" />);
await screen.findByText(/failed to load/i);
});
});When components use React Context, wrap them in the providers:
// src/test/helpers.tsx
import { render } from "@testing-library/react";
import { AuthProvider } from "@/context/auth";
function AllProviders({ children }: { children: React.ReactNode }) {
return (
<AuthProvider>
{children}
</AuthProvider>
);
}
function customRender(ui: React.ReactElement, options = {}) {
return render(ui, { wrapper: AllProviders, ...options });
}
export { customRender as render };
// Usage in tests
import { render } from "@/test/helpers";// getBy* — throws if not found (use for elements that must exist)
screen.getByRole("button", { name: "Submit" })
screen.getByLabelText("Email")
screen.getByPlaceholderText("Enter email")
screen.getByText("Hello World")
screen.getByTestId("submit-btn")
// queryBy* — returns null if not found (use for elements that might not exist)
const errorMsg = screen.queryByText("Error!");
expect(errorMsg).not.toBeInTheDocument();
// findBy* — async, waits for element to appear
const result = await screen.findByText("Loaded!");
// getAllBy* / queryAllBy* / findAllBy* — multiple elements
const items = screen.getAllByRole("listitem");
expect(items).toHaveLength(5);Focus tests on: business logic, user interactions, edge cases, async behavior, and the most critical user flows.
Next lesson: Deploy to Vercel production — CI/CD setup, environment variables, and monitoring.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises