Component Testing

Vitest ve React Testing Library kurulumu, render+screen ile ilk test, getByRole/getByLabelText ile sorgulama, jest-dom matcher'ları, ve koşullu render'ı test etmek -- basit örneklerle.

Orta 12 dk
EN

Component Testing

Şimdiye kadar yazdığımız her component'i TARAYICIDA elle tıklayarak kontrol ettik. Bu, birkaç component için yeterli olsa da, uygulama büyüdükçe her değişiklikten sonra her ekranı elle kontrol etmek hem yavaş hem de güvenilmez. Bu ders, component'lerin doğru çalıştığını OTOMATİK olarak, kod ile doğrulamayı öğretiyor.

Vitest ve React Testing Library Kurulumu

Bu kursta iki kütüphane kullanıyoruz:

  • Vitest — testleri ÇALIŞTIRAN araç (describe, it, expect gibi fonksiyonları sağlar). Vite tabanlı projeler için tasarlandığı için ek bir yapılandırmaya neredeyse hiç ihtiyaç duymaz.
  • React Testing Library (RTL) — component'leri sahte bir DOM'a (jsdom) YERLEŞTİRİP, o DOM'u gerçek bir kullanıcının göreceği şekilde SORGULAMAMIZI sağlayan kütüphane.

Bir Vite projesine eklemek için:

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

vite.config.js içine bir test bloğu eklenir:

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

environment: "jsdom" testlerin gerçek bir tarayıcı yerine, Node içinde ÇALIŞAN sahte bir DOM'da koşmasını sağlar. setupFiles içindeki dosyada tek bir satır yeterli:

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

Bu satır, birazdan göreceğimiz toBeInTheDocument() gibi ek doğrulamaları (matcher) Vitest'in expect'ine EKLER.

render() ve screen ile İlk Testimiz

Bir testin en temel iskeleti; component'i sahte DOM'a yerleştirmek ve içinde beklediğimiz bir şeyin olduğunu doğrulamaktan oluşur:

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, ilgili testleri bir grup altında toplar; it (ya da test), tek bir test senaryosunu tanımlar. render(<Counter />), component'i jsdom'a yerleştirir. screen, o anki DOM'u SORGULAMAK için kullanılır -- getByText, verilen metni içeren bir eleman bulamazsa test ANINDA başarısız olur.

getByRole ve getByLabelText ile Sorgulama

getByText her zaman en doğru sorgu değildir -- RTL, gerçek kullanıcıların (ve ekran okuyucuların) sayfayı nasıl ALGILADIĞINA daha yakın sorgular sunar:

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 }), bir <button> elemanını ERİŞİLEBİLİRLİK rolünden ve görünen adından bulur -- RTL'in resmî dokümantasyonu, mümkün olduğunda getByRole'ü ÖNCELİKLİ sorgu olarak önerir. getByLabelText("Name"), <label htmlFor="name"> ile eşleşen input'u, id veya test-id eklemeye gerek kalmadan bulur.

jest-dom Matcher'ları

Kurulumda eklediğimiz @testing-library/jest-dom/vitest, expect'e DOM'a özel yeni doğrulamalar ekler:

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() ve toBeEnabled(), bir elemanın disabled özniteliğini kontrol eder; toBeInTheDocument() bir elemanın DOM'da var olup olmadığını doğrular. Bunlar düz Vitest'te YOKTUR -- jest-dom paketinin eklediği, DOM testleri için özel olarak tasarlanmış matcher'lardır.

Koşullu Render'ı Test Etmek

State & Events dersinde gördüğümüz koşullu render deseni, en sık test edilen senaryolardan biridir -- her durumun DOĞRU metni gösterdiğini ayrı ayrı doğrularız:

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

Üç ayrı it bloğu, status prop'unun üç farklı değeri için component'i ayrı ayrı render edip doğru mesajın göründüğünü kontrol ediyor. İlk testte ayrıca queryByText kullanılıyor: getByText'in aksine, eleman bulunamazsa hata FIRLATMAZ, null döner -- bu yüzden bir şeyin EKRANDA OLMADIĞINI doğrulamak için getByText değil queryByText kullanılır.

Özet ve Terimler Sözlüğü

Vitest testleri ÇALIŞTIRIR, React Testing Library component'leri sahte bir DOM'a yerleştirip SORGULAMAMIZI sağlar. render() bir component'i DOM'a yerleştirir; screen o DOM'u sorgulamak için kullanılır. getByRole/getByLabelText/getByText, bir eleman bulamazsa hata fırlatır; queryBy* varyantları bulamazsa null döner ve bir şeyin EKRANDA OLMADIĞINI doğrulamak için kullanılır. @testing-library/jest-dom, toBeInTheDocument() gibi DOM'a özel matcher'lar ekler.

Terimler Sözlüğü

Test Runner — Testleri bulup çalıştıran, sonuçları raporlayan araç (Vitest).

jsdom — Node içinde çalışan, gerçek bir tarayıcıyı SİMÜLE eden sahte bir DOM ortamı.

Matcherexpect(...)'ten sonra zincirlenen, belirli bir koşulu doğrulayan fonksiyon (toBeInTheDocument(), toHaveValue() gibi).