Component Testing

Setting up Vitest and React Testing Library, your first test with render+screen, querying with getByRole/getByLabelText, jest-dom matchers, and testing conditional rendering -- with simple examples.

Intermediate 12 min
TR

Component Testing

So far we've checked every component by clicking around in the browser by hand. That's fine for a handful of components, but as an app grows, manually re-checking every screen after every change is slow and unreliable. This lesson teaches you to verify that components work correctly AUTOMATICALLY, with code.

Setting Up Vitest and React Testing Library

We use two libraries in this course:

  • Vitest — the tool that RUNS tests (it provides functions like describe, it, expect). Since it's built for Vite projects, it needs almost no extra configuration.
  • React Testing Library (RTL) — the library that lets you MOUNT a component into a fake DOM (jsdom) and then QUERY that DOM the way a real user would see it.

To add them to a Vite project:

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

Add a test block to vite.config.js:

export default defineConfig({
  plugins: [react()],
  test: {
    environment: "jsdom",
    setupFiles: ["./src/setupTests.js"],
    globals: true,
  },
});

environment: "jsdom" makes tests run against a fake DOM inside Node instead of a real browser. The setup file only needs one line:

import "@testing-library/jest-dom/vitest";

This line ADDS extra assertions (like the toBeInTheDocument() we'll see shortly) to Vitest's expect.

Our First Test with render() and screen

The most basic test skeleton mounts a component into the fake DOM and checks that something we expect is there:

import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";

// Test edilecek component. Gerçek bir projede bu genelde ayrı bir dosyada
// (Counter.jsx) olur, testi de ayrı bir dosyada (Counter.test.jsx) yazılır --
// burada tek bir okunabilir örnek olması için ikisini birleştirdik.
function Counter() {
  return (
    <div>
      <p>Count: 0</p>
    </div>
  );
}

describe("Counter", () => {
  it("renders the initial count", () => {
    // render(), component'i gerçek bir DOM'a (jsdom, tarayıcı SİMÜLASYONU) yerleştirir.
    render(<Counter />);

    // screen, o anki DOM'u SORGULAMAK için kullanılır. getByText, tam olarak bu
    // metni içeren bir eleman bulamazsa testi ANINDA başarısız yapar.
    expect(screen.getByText("Count: 0")).toBeInTheDocument();
  });
});

describe groups related tests together; it (or test) defines a single test case. render(<Counter />) mounts the component into jsdom. screen is used to QUERY that DOM -- getByText immediately fails the test if it can't find an element containing the given text.

Querying with getByRole and getByLabelText

getByText isn't always the best query -- RTL offers queries that are closer to how a real user (or a screen reader) PERCEIVES the page:

import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";

function LoginButton() {
  return <button>Log In</button>;
}

function NameField() {
  return (
    <div>
      <label htmlFor="name">Name</label>
      <input id="name" defaultValue="Ada" />
    </div>
  );
}

describe("Querying elements", () => {
  it("finds a button by its accessible role and name", () => {
    render(<LoginButton />);

    // getByRole, elemanları GÖRÜNEN metinden değil, ERİŞİLEBİLİRLİK rolünden
    // bulur -- bir <button>, "button" rolüne sahiptir. Bu, gerçek kullanıcıların
    // (ve ekran okuyucuların) sayfayı nasıl algıladığına en yakın sorgu şeklidir.
    expect(screen.getByRole("button", { name: /log in/i })).toBeInTheDocument();
  });

  it("finds a form field by its connected label", () => {
    render(<NameField />);

    // getByLabelText, <label htmlFor="..."> ile eşleşen input'u bulur --
    // input'un id'sini veya bir test-id eklemeye gerek kalmaz.
    expect(screen.getByLabelText("Name")).toHaveValue("Ada");
  });
});

getByRole("button", { name: /log in/i }) finds a <button> by its accessibility role and visible name -- RTL's official docs recommend getByRole as the PREFERRED query whenever possible. getByLabelText("Name") finds the input connected to <label htmlFor="name">, with no need to add an id or a test-id.

jest-dom Matchers

The @testing-library/jest-dom/vitest import we added during setup adds new DOM-specific assertions to expect:

import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";

function SubmitButton({ disabled }) {
  return <button disabled={disabled}>Submit</button>;
}

describe("SubmitButton", () => {
  it("is disabled when the disabled prop is true", () => {
    render(<SubmitButton disabled={true} />);

    // toBeDisabled/toBeEnabled/toBeInTheDocument, @testing-library/jest-dom'un
    // eklediği matcher'lardır -- düz Vitest'te yok, jsdom kullanan projelerde
    // ayrıca kurulur (setupFiles içinde "@testing-library/jest-dom/vitest").
    expect(screen.getByRole("button", { name: /submit/i })).toBeDisabled();
  });

  it("is enabled when the disabled prop is false", () => {
    render(<SubmitButton disabled={false} />);

    expect(screen.getByRole("button", { name: /submit/i })).toBeEnabled();
  });
});

toBeDisabled() and toBeEnabled() check an element's disabled attribute; toBeInTheDocument() verifies whether an element exists in the DOM at all. None of these exist in plain Vitest -- they're matchers added by the jest-dom package specifically for testing the DOM.

Testing Conditional Rendering

The conditional-rendering pattern from State & Events is one of the most commonly tested scenarios -- we verify that each state shows the CORRECT text:

import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";

// State & Events dersindeki koşullu render deseninin test edilmiş hali.
function StatusMessage({ status }) {
  if (status === "loading") return <p>Loading...</p>;
  if (status === "error") return <p>Something went wrong.</p>;
  return <p>Data loaded successfully.</p>;
}

describe("StatusMessage", () => {
  it("shows a loading message", () => {
    render(<StatusMessage status="loading" />);
    expect(screen.getByText("Loading...")).toBeInTheDocument();

    // queryByText, getByText'in aksine bulamazsa HATA FIRLATMAZ -- null döner.
    // Bir şeyin EKRANDA OLMADIĞINI doğrulamak için queryBy* kullanılır.
    expect(screen.queryByText("Data loaded successfully.")).not.toBeInTheDocument();
  });

  it("shows an error message", () => {
    render(<StatusMessage status="error" />);
    expect(screen.getByText("Something went wrong.")).toBeInTheDocument();
  });

  it("shows the success message by default", () => {
    render(<StatusMessage status="success" />);
    expect(screen.getByText("Data loaded successfully.")).toBeInTheDocument();
  });
});

Three separate it blocks render the component with three different values of the status prop and check that the right message shows up each time. The first test also uses queryByText: unlike getByText, it does NOT throw if the element isn't found -- it returns null -- which is why queryBy* (not getByText) is used to assert that something is ABSENT from the screen.

Summary and Glossary

Vitest RUNS tests; React Testing Library lets you mount components into a fake DOM and QUERY them. render() mounts a component into the DOM; screen is used to query that DOM. getByRole / getByLabelText / getByText throw if they can't find an element; the queryBy* variants return null instead and are used to assert that something is ABSENT. @testing-library/jest-dom adds DOM-specific matchers like toBeInTheDocument().

Glossary

Test Runner — The tool that discovers and runs tests and reports the results (Vitest).

jsdom — A fake DOM environment that runs inside Node and SIMULATES a real browser.

Matcher — A function chained after expect(...) that verifies a specific condition (toBeInTheDocument(), toHaveValue(), etc.).