- Регистрация
- 1 Мар 2015
- Сообщения
- 1,481
- Баллы
- 155
That line stuck with me the first time I read it. As someone who used to assume my components were flawless if they “looked fine” in the browser, I’ve come a long way. I was introduced to testing during my time working on Naasa X, a trading management system where reliability was non-negotiable. That experience reshaped how I approach frontend development, especially with React and Next.js.You don’t really know if your component works — until you write a test that proves it.
In this post, I want to walk you through how I write unit tests for my components using Jest and React Testing Library. This isn't a deep theoretical dive — just a practical guide based on how I test components in my real-world projects.
? Tools I Use
- Jest - JavaScript testing framework
- React - Testing Library — for writing tests that resemble real user behavior
npm install --save-dev jest @testing-library/react @testing-library/jest-dom @testing-library/user-event
Then create and add this to your jest.config.ts:
import type { Config } from "jest";
import nextJest from "next/jest.js";
const createJestConfig = nextJest({
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
dir: "./",
});
// Add any custom config to be passed to Jest
const config: Config = {
coverageProvider: "v8",
testEnvironment: "jsdom",
// Add more setup options before each test is run
setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
};
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
export default createJestConfig(config);
Create jest.setup.ts
This sets up the testing environment globally:
import "@testing-library/jest-dom";
? Component to Test — LoginForm
Here’s a simple LoginForm built with Shadcn UI components, react-hook-form, and zod for validation:
"use client";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
const schema = z.object({
email: z.string().nonempty("Field is required"),
password: z.string().nonempty("Field is required"),
});
type FormData = z.infer<typeof schema>;
export function LoginForm({
className,
onSubmit = () => {},
}: {
className?: string;
onSubmit?: (data: FormData) => void;
}) {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormData>({
resolver: zodResolver(schema),
});
const handleFormSubmit = (data: FormData) => {
onSubmit(data);
};
return (
<div className={cn("flex flex-col gap-6", className)}>
<Card>
<CardHeader>
<CardTitle className="text-2xl">Login</CardTitle>
<CardDescription>
Enter your email below to login to your account
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit(handleFormSubmit)}>
<div className="flex flex-col gap-6">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="m@example.com"
{...register("email")}
/>
{errors.email && (
<p className="text-sm text-red-500">{errors.email.message}</p>
)}
</div>
<div className="grid gap-2">
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
{...register("password")}
/>
{errors.password && (
<p className="text-sm text-red-500">
{errors.password.message}
</p>
)}
</div>
<Button type="submit" className="w-full">
Login
</Button>
</div>
</form>
</CardContent>
</Card>
</div>
);
}
? Writing Unit Tests for LoginForm
Here’s how I write unit tests for this component:
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import "@testing-library/jest-dom";
import userEvent from "@testing-library/user-event";
import { LoginForm } from ".";
describe("LoginForm", () => {
const mockOnSubmit = jest.fn();
it("renders the login form", () => {
render(<LoginForm />);
expect(screen.getByLabelText("Email")).toBeInTheDocument();
expect(screen.getByLabelText("Password")).toBeInTheDocument();
expect(screen.getByRole("button", { name: /login/i })).toBeInTheDocument();
});
it("validates required fields", async () => {
render(<LoginForm />);
fireEvent.click(screen.getByRole("button", { name: /login/i }));
await waitFor(() => {
expect(screen.getAllByText("Field is required")).toHaveLength(2);
});
});
it("logs in successfully with valid credentials", async () => {
render(<LoginForm onSubmit={mockOnSubmit} />);
const emailInput = screen.getByLabelText("Email");
const passwordInput = screen.getByLabelText("Password");
const loginButton = screen.getByRole("button", { name: "Login" });
await userEvent.type(emailInput, "test@example.com");
await userEvent.type(passwordInput, "password123");
await userEvent.click(loginButton);
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith({
email: "test@example.com",
password: "password123",
});
});
});
});
? Recommended Folder Structure for Testing
/app
/components
/LoginForm
index.tsx ← Component
LoginForm.test.tsx ← Unit test for the component
/tests
jest.setup.ts ← Global test setup
jest.config.ts ← Jest configuration file
? Breaking Down the Tests
Let’s unpack what’s happening in the test file:
Render Test: Checks if email, password, and login button appear in the form.
Validation Test: Simulates clicking submit with empty fields and checks for required field messages.
Submit Test: Fills in valid input, submits the form, and confirms the onSubmit function is called with the correct data.
This approach ensures the component works as intended from a user’s perspective, which is critical for real-world apps.
? A Few Tips I’ve Learned
- Test user behavior, not implementation details.
- Keep your tests simple and readable — your future self will thank you.
- Don’t over-test, focus on key paths: rendering, validation, and success states.
? What’s Next?
This post focused on testing a simple form component. But in real-world apps, we often deal with API calls, loading states, errors, and authentication. That’s where mocking and mock servers come into play — and I’ll dive into that in an upcoming post.
? Final Thought
I used to build components and hope they worked. Now I build with confidence — because my tests prove they do. If you’re new to testing in Next.js, start small like this. You’ll be surprised how much peace of mind a few tests can bring.
Let me know if you found this helpful. Happy testing!