React Testing for Beginners! βš’οΈπŸš€

Akash PrasadAkash Prasad
8 min read

What is Testing?

So, Testing in software development refers to analyzing your code and ensuring that it does not break in any scenarios or edge cases that are difficult to recognize during development. By testing the application, we ensure that it will be completely stable in production.

There are mainly three types of testing:

  • Unit Testing

    In unit testing, we test each section of our application separately.

  • Integration Testing

    In Integration testing as the name suggests we test multiple units or sections of our application by connecting them together as they will in final production mode.

  • E2E

    E2E means end-to-end testing, when the development of the application is completed then this testing is performed.

Testing Library

There are various testing libraries out there but in this article we will learn about the current popular one Vitest, which is used by almost everyone in the industry. Vitest is a fast unit-testing framework built for Vite and designed as a drop-in replacement for Jest. It is optimized for speed and works well with modern frontend frameworks like Vue, React, and Svelte.

Key Features of Vitest

βœ… Lightning Fast – Uses Vite's native ESM and pre-bundling for faster test execution.
βœ… Jest-Compatible API – If you're familiar with Jest, switching is easy.
βœ… Built-in TypeScript Support – No extra configuration needed.

Sample Test

Install Vitest in your project:

npm install -D vitest

Update package.json scripts:

"scripts": {
  "test": "vitest"
}

Run tests:

npm test

Basic Example

import { describe, expect, it } from 'vitest';

describe('Math operations', () => {
  it('adds numbers correctly', () => {
    expect(1 + 2).toBe(3);
  });
});

Vitest provides a Jest-like API with core functions such as describe, it (or test), and expect to structure and run test cases.

1. describe – Grouping Tests

The describe function is used to group related tests together. It helps organize test suites logically.

Example:

import { describe, it, expect } from 'vitest';

describe('Math operations', () => {
  it('adds numbers correctly', () => {
    expect(1 + 2).toBe(3);
  });

  it('multiplies numbers correctly', () => {
    expect(2 * 3).toBe(6);
  });
});

πŸ’‘ Everything inside describe() is considered a test suite.


2. it – Defines a Single Test Case

it (or test) is used to define an individual test case.

it('checks if 10 is greater than 5', () => {
  expect(10).toBeGreaterThan(5);
});

You can also use test() instead of it(), as both are aliases.


3. expect – Assertions for Checking Values

expect is used to make assertions about test outcomes.

βœ… Basic Assertions:

expect(2 + 2).toBe(4); // Checks exact match
expect([1, 2]).toEqual([1, 2]); // Checks deep equality

βœ… Checking for Truthiness:

expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect(true).toBeTruthy();
expect(false).toBeFalsy();

βœ… Number Comparisons:

expect(10).toBeGreaterThan(5);
expect(3.14).toBeCloseTo(3.1, 1); // Compares with precision

βœ… String Matching:

expect('hello world').toMatch(/world/);

βœ… Array & Object Matching:

expect([1, 2, 3]).toContain(2);
expect({ name: 'Akash' }).toHaveProperty('name', 'Akash');

Testing React component

Vitest works seamlessly with the React testing library to test React components.

Install Dependencies

npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom

Key Testing Concepts

βœ… render() – Renders the component in a virtual DOM.
βœ… screen.getByText() – Finds an element with matching text.
βœ… fireEvent.click() – Simulates a click event.
βœ… expect(...).toBeInTheDocument() – Checks if an element is present.
βœ… vi.fn() – Mocks a function to track calls.
βœ… expect(fn).toHaveBeenCalledTimes(n) – Verifies function execution.


Let’s test a simple component

const Greet = ({ name }: { name?: string }) => {
  if (name) return <h1>Hello {name}</h1>;

  return <button>Login</button>;
};

export default Greet;

Test for this component will be like

import { render, screen } from "@testing-library/react";
import { it, expect, describe } from "vitest";
import Greet from "../components/Greet";
import "@testing-library/jest-dom/vitest";

describe("Greet", () => {
  it("should Display name if name is passed unless show login", () => {
    render(<Greet name="Akash" />);
    const heading = screen.getByRole("heading");
    expect(heading).toBeInTheDocument();
    expect(heading).toHaveTextContent(/akash/i);
  });

  it("should render login button when name is not passed", () => {
    render(<Greet />);
    const loginBtn = screen.getByRole("button");
    expect(loginBtn).toBeInTheDocument();
    expect(loginBtn).toHaveTextContent(/login/i);
  });
});

Testing a component having conditionally rendered content

 type User = {
  id: number;
  name: string;
  isAdmin?: boolean;
};

const UserAccount = ({ user }: { user: User }) => {
  return (
    <>
      <h2>User Profile</h2>
      {user.isAdmin && <button>Edit</button>}
      <div>
        <strong>Name:</strong> {user.name}
      </div>
    </>
  );
};

export default UserAccount;

Test

import { render, screen } from "@testing-library/react";
import UserAccount from "../components/UserAccount";

describe("UserAccont", () => {
  it("should display edit button if admin", () => {
    render(
      <UserAccount
        user={{
          id: 1,
          name: "Akash",
          isAdmin: true,
        }}
      />
    );

    const editBtn = screen.getByRole("button");
    expect(editBtn).toBeInTheDocument();
    expect(editBtn).toHaveTextContent("Edit");
  });

  it("should not display edit button if not admin", () => {
    render(
      <UserAccount
        user={{
          id: 1,
          name: "Akash",
          isAdmin: false,
        }}
      />
    );
    const editBtn = screen.queryByRole("button");
    expect(editBtn).not.toBeInTheDocument();
  });

  it("should diplay user name", () => {
    render(
      <UserAccount
        user={{
          id: 1,
          name: "Akash",
          isAdmin: false,
        }}
      />
    );
    expect(screen.getByText("Akash")).toBeInTheDocument();
  });
});

Testing List

 type User = {
  id: number;
  name: string;
  isAdmin?: boolean;
};

const UserList = ({ users }: { users: User[] }) => {
  if (users.length === 0) return <p>No users available.</p>;

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>
          <a href={`/users/${user.id}`}>{user.name}</a>
        </li>
      ))}
    </ul>
  );
};

export default UserList;
import UserList from "../components/UserList";
import { User } from "../entities";
import { render, screen } from "@testing-library/react";

describe("User List", () => {
  it("should print no user available if user[] is empty", () => {
    const users: User[] = [];
    render(<UserList users={users} />);
    const msg = screen.getByRole("paragraph");
    expect(msg).toBeInTheDocument();
    expect(msg).toHaveTextContent(/no users available/i);
  });

  it("should print no user available if user[] is empty", () => {
    const users: User[] = [];
    render(<UserList users={users} />);
    const msg = screen.getByRole("paragraph");
    expect(msg).toBeInTheDocument();
    expect(msg).toHaveTextContent(/no users available/i);
  });

  it("should print list of users", () => {
    const users: User[] = [
      { id: 1, name: "Akash" },
      { id: 2, name: "John" },
    ];
    render(<UserList users={users} />);
    users.forEach((list) => {
      const link = screen.getByRole("link", { name: list.name });
      expect(link).toBeInTheDocument();
      expect(link).toHaveAttribute("href", `/users/${list.id}`);
    });
  });
});

Testing a component having image

const ProductImageGallery = ({ imageUrls }: { imageUrls: string[] }) => {
  if (imageUrls.length === 0) return null;

  return (
    <ul>
      {imageUrls.map((url) => (
        <li key={url}>
          <img src={url} />
        </li>
      ))}
    </ul>
  );
};

export default ProductImageGallery;
import { render, screen } from "@testing-library/react";
import ProductImageGallery from "../components/ProductImageGallery";
describe("ProductImage", () => {
  it("should dispaly nothing if imageUrl[] is empty", () => {
    const { container } = render(<ProductImageGallery imageUrls={[]} />);
    expect(container).toBeEmptyDOMElement();
  });

  it("should list of imageUrls", () => {
    const imageUrls: string[] = [
      "https://www.google.com",
      "https://www.github.com",
    ];
    render(<ProductImageGallery imageUrls={imageUrls} />);
    const images = screen.getAllByRole("img");
    images.forEach((img, idx) => {
      expect(images[idx]).toHaveAttribute("src", imageUrls[idx]);
    });
  });
});

Testing User Interaction

import { useState } from "react";

const TermsAndConditions = () => {
  const [isChecked, setIsChecked] = useState(false);

  return (
    <div>
      <h1>Terms & Conditions</h1>
      <p>
        Lorem ipsum dolor, sit amet consectetur adipisicing elit. Autem,
        delectus.
      </p>
      <div className="pb-3">
        <label htmlFor="agree">
          <input
            type="checkbox"
            id="agree"
            checked={isChecked}
            onChange={() => setIsChecked(!isChecked)}
            className="mr-1"
          />
          I accept the terms and conditions.
        </label>
      </div>
      <button disabled={!isChecked} className="btn">
        Submit
      </button>
    </div>
  );
};

export default TermsAndConditions;
import { render, screen } from "@testing-library/react";
import TermsAndConditions from "../components/TermsAndConditions";
import userEvent from "@testing-library/user-event";

describe("Terms&Conditions", () => {
  it("should display heading and render checkbox", () => {
    render(<TermsAndConditions />);
    const heading = screen.getByRole("heading");
    expect(heading).toBeInTheDocument();
    expect(heading).toHaveTextContent(/terms & conditions/i);

    const checkbox = screen.getByRole("checkbox");
    expect(checkbox).toBeInTheDocument();
    expect(checkbox).not.toBeChecked();

    const button = screen.getByRole("button");
    expect(button).toBeInTheDocument();
    expect(button).toHaveTextContent(/submit/i);
    expect(button).toBeDisabled();
  });

  it("should be enabled when checkbox is checked", async () => {
    render(<TermsAndConditions />);
    const checkbox = screen.getByRole("checkbox");
    const user = userEvent.setup();

    await user.click(checkbox);
    const button = screen.getByRole("button");
    expect(button).toBeInTheDocument();
    expect(button).toBeEnabled();
  });
});

Some more User Interaction Testing

import { useState } from "react";

const ExpandableText = ({ text }: { text: string }) => {
  const limit = 255;
  const [isExpanded, setExpanded] = useState(false);

  if (text.length <= limit) return <article>{text}</article>;

  return (
    <div>
      {isExpanded ? (
        <article>{text}</article>
      ) : (
        <article>{text.substring(0, limit)}...</article>
      )}
      <button className="btn" onClick={() => setExpanded(!isExpanded)}>
        {isExpanded ? "Show Less" : "Show More"}
      </button>
    </div>
  );
};

export default ExpandableText;
import { render, screen } from "@testing-library/react";
import ExpandableText from "../components/ExpandableText";
import userEvent from "@testing-library/user-event";

describe("Expandable Text", () => {
  it("should display only article if text under limit", () => {
    render(<ExpandableText text="hello world!" />);
    expect(screen.getByRole("article")).toBeInTheDocument();
    expect(screen.queryByRole("button")).not.toBeInTheDocument();
  });

  it("should display only 255 characters if show more is not clicked", async () => {
    const text =
      "Lorem ipsum dolor, sit amet consectetur adipisicing elit. Vel autem molestiae doloribus voluptates natus repellendus tempora consectetur laboriosam? Quam distinctio odio nobis consequuntur repellendus possimus ab saepe minus. Nam eum non pariatur, in nulla nisi harum asperiores delectus quod animi, voluptates officia consequuntur, eveniet est porro quidem ullam? Ex suscipit deleniti delectus quas voluptatem quibusdam minus modi dicta, natus, inventore adipisci ipsam dolores voluptates.";
    render(<ExpandableText text={text} />);
    const trucatedtext = text.substring(0, 255) + "...";
    const showMore = screen.getByRole("button");
    expect(showMore).toBeInTheDocument();
    expect(screen.getByRole("article")).toBeInTheDocument();
    expect(screen.getByText(trucatedtext)).toBeInTheDocument();
    expect(showMore).toHaveTextContent(/show more/i);

    const user = userEvent.setup();
    await user.click(showMore);

    expect(showMore).toHaveTextContent(/show less/i);
    expect(screen.getByRole("article")).toBeInTheDocument();
    expect(screen.getByRole("article")).toHaveTextContent(text);

    await user.click(showMore);
    expect(screen.getByRole("article")).toBeInTheDocument();
    expect(screen.getByText(trucatedtext)).toBeInTheDocument();
    expect(showMore).toHaveTextContent(/show more/i);
  });
});

Final Thoughts

Vitest provides a fast and efficient way to test React applications. It offers a Jest-compatible API while leveraging the power of Vite. By integrating Vitest with the React Testing Library, developers can ensure their applications remain robust, scalable, and free from unexpected bugs.

0
Subscribe to my newsletter

Read articles from Akash Prasad directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Akash Prasad
Akash Prasad

I am a Full Stack Developer and an AI enthusiast!