DEV Community

Cover image for How I Test My Next.js Components — A Practical Guide with Jest and React Testing Library
Avishek Raut
Avishek Raut

Posted on

How I Test My Next.js Components — A Practical Guide with Jest and React Testing Library

You don’t really know if your component works — until you write a test that proves it.

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.

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

✅ Setup: Installing the Required Packages

npm install --save-dev jest @testing-library/react @testing-library/jest-dom @testing-library/user-event

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

Create jest.setup.ts

This sets up the testing environment globally:

import "@testing-library/jest-dom";
Enter fullscreen mode Exit fullscreen mode

🔨 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>
  );
}

Enter fullscreen mode Exit fullscreen mode

🧪 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",
      });
    });
  });
});

Enter fullscreen mode Exit fullscreen mode

📁 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

Enter fullscreen mode Exit fullscreen mode

🔍 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!

Top comments (0)