User Interaction Testing

user-event ile gerçekçi etkileşim simülasyonu, tıklamayı ve yazmayı test etmek, form gönderimini vi.fn() ile doğrulamak, ve asenkron UI güncellemelerini findBy* ile test etmek -- basit örneklerle.

Orta 11 dk
EN

User Interaction Testing

Component Testing dersinde, bir component'in DOĞRU render olduğunu test etmeyi öğrendik -- ama React uygulamalarının çoğu, kullanıcı bir şeye TIKLAYANA, bir şey YAZANA ya da bir form GÖNDERENE kadar "durağan"dır. Bu ders, kullanıcı ETKİLEŞİMLERİNİ test etmeyi anlatıyor.

user-event ile Gerçekçi Etkileşim Simülasyonu

React Testing Library ile birlikte, kullanıcı etkileşimlerini simüle etmek için @testing-library/user-event paketi kullanılır:

npm install -D @testing-library/user-event

RTL'in kendi fireEvent API'si de bir tıklama/yazma tetikleyebilir, ama fireEvent tek bir DOM olayını (click gibi) doğrudan gönderir. user-event ise gerçek bir kullanıcının tıklarken/yazarken tetiklediği ARA adımları da (hover, focus, pointer olayları) simüle eder -- bu yüzden RTL'in resmî dokümantasyonu artık fireEvent yerine user-event'i ÖNERİYOR.

Tıklamayı Test Etmek

State & Events dersindeki Counter component'ini, bu kez gerçek bir tıklama simüle ederek test edelim:

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

function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

describe("Counter interaction", () => {
  it("increments the count when the button is clicked", async () => {
    // userEvent.setup(), gerçek bir kullanıcının tıklamasına fireEvent'ten
    // daha yakın davranan bir "kullanıcı" nesnesi oluşturur (hover, focus gibi
    // ara adımları da simüle eder). Bu yüzden RTL artık userEvent'i ÖNERİYOR.
    const user = userEvent.setup();
    render(<Counter />);

    expect(screen.getByText("Count: 0")).toBeInTheDocument();

    // userEvent'in metotları ASENKRON'dur -- her zaman await edilmeli.
    await user.click(screen.getByRole("button", { name: /increment/i }));

    expect(screen.getByText("Count: 1")).toBeInTheDocument();
  });
});

userEvent.setup(), bir "kullanıcı" nesnesi oluşturur. Bu nesnenin metotları (click, type gibi) HER ZAMAN asenkrondur ve await edilmelidir -- unutursak test, tıklama tamamlanmadan bir sonraki satıra geçer ve yanlış (eski) bir DOM durumunu kontrol eder.

Yazmayı Test Etmek

Forms dersindeki controlled input deseni, user.type ile test edilir:

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

function NameInput() {
  const [name, setName] = useState("");
  return (
    <div>
      <label htmlFor="name">Name</label>
      <input id="name" value={name} onChange={(e) => setName(e.target.value)} />
      <p>You typed: {name}</p>
    </div>
  );
}

describe("NameInput interaction", () => {
  it("updates the displayed text as the user types", async () => {
    const user = userEvent.setup();
    render(<NameInput />);

    const input = screen.getByLabelText("Name");

    // user.type, verilen metni HARF HARF yazar -- her tuş vuruşu, controlled
    // component'teki onChange'i gerçek yazmaya çok benzer şekilde tetikler.
    await user.type(input, "Ada");

    expect(screen.getByText("You typed: Ada")).toBeInTheDocument();
    expect(input).toHaveValue("Ada");
  });
});

user.type(input, "Ada"), verilen metni HARF HARF yazar -- her tuş vuruşu, controlled component'teki onChange'i gerçek bir klavyeyle yazmaya çok benzer şekilde tetikler. Testin sonunda hem ekrandaki metni (getByText) hem input'un kendi değerini (toHaveValue) kontrol ediyoruz.

Form Gönderimini Test Etmek

Bir formu doldurup göndermek, user.type ve user.click'in birlikte kullanıldığı en yaygın senaryodur:

import { useState } from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi } from "vitest";

function SignupForm({ onSubmitted }) {
  const [email, setEmail] = useState("");

  function handleSubmit(e) {
    e.preventDefault();
    onSubmitted(email);
  }

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="email">Email</label>
      <input id="email" value={email} onChange={(e) => setEmail(e.target.value)} />
      <button type="submit">Sign Up</button>
    </form>
  );
}

describe("SignupForm submission", () => {
  it("calls onSubmitted with the typed email when the form is submitted", async () => {
    const user = userEvent.setup();

    // vi.fn(), gerçek bir prop yerine geçen SAHTE bir fonksiyondur -- hangi
    // argümanlarla, kaç kez çağrıldığını sonradan sorgulayabiliriz.
    const handleSubmitted = vi.fn();
    render(<SignupForm onSubmitted={handleSubmitted} />);

    await user.type(screen.getByLabelText("Email"), "ada@example.com");
    await user.click(screen.getByRole("button", { name: /sign up/i }));

    expect(handleSubmitted).toHaveBeenCalledWith("ada@example.com");
    expect(handleSubmitted).toHaveBeenCalledTimes(1);
  });
});

vi.fn(), gerçek bir prop yerine geçen SAHTE bir fonksiyon oluşturur -- component'in dışına hiçbir gerçek istek gitmeden, bu fonksiyonun hangi ARGÜMANLARLA, KAÇ KEZ çağrıldığını doğrulayabiliriz. toHaveBeenCalledWith(...) ve toHaveBeenCalledTimes(...), bu sahte fonksiyonlara özel matcher'lardır.

Asenkron UI Güncellemelerini Test Etmek

Hooks dersindeki useEffect deseniyle, bir component zaman içinde KENDİLİĞİNDEN güncellenebilir (bir fetch isteğinin tamamlanması gibi). Bu tür güncellemeleri test etmek için findBy* sorguları kullanılır:

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

// Hooks dersindeki useEffect deseni -- component mount olduktan bir süre
// sonra kendi state'ini güncelliyor (gerçek bir uygulamada bu, bir fetch
// isteğinin tamamlanması olurdu; burada basit tutmak için setTimeout).
function DelayedGreeting() {
  const [ready, setReady] = useState(false);

  useEffect(() => {
    const timer = setTimeout(() => setReady(true), 50);
    return () => clearTimeout(timer);
  }, []);

  if (!ready) return <p>Loading...</p>;
  return <p>Welcome!</p>;
}

describe("DelayedGreeting async update", () => {
  it("shows loading first, then the greeting once ready", async () => {
    render(<DelayedGreeting />);

    // İlk render'da hâlâ "Loading..." görünüyor.
    expect(screen.getByText("Loading...")).toBeInTheDocument();

    // findByText, getByText'in ASENKRON hâlidir: eleman hemen yoksa hata
    // fırlatmaz, belirli bir süre (varsayılan 1000ms) boyunca tekrar tekrar
    // dener. DOM'u zamanla değişen (fetch, timer, animasyon sonrası) her şeyi
    // test etmenin doğru yolu budur -- waitFor de aynı amaçla kullanılabilir.
    const greeting = await screen.findByText("Welcome!");
    expect(greeting).toBeInTheDocument();
  });
});

getByText (ve queryByText), DOM'u SADECE O AN kontrol eder -- eleman henüz yoksa test başarısız olur. findByText ise ASENKRON'dur: eleman hemen yoksa hata fırlatmaz, belirli bir süre boyunca (varsayılan 1000ms) tekrar tekrar dener ve eleman görününce test'e devam eder. DOM'u zamanla değişen (fetch, timer, animasyon sonrası) her şeyi test etmenin doğru yolu budur; aynı amaç için waitFor(...) de kullanılabilir.

Özet ve Terimler Sözlüğü

@testing-library/user-event, fireEvent'ten daha GERÇEKÇİ bir etkileşim simülasyonu sunar; userEvent.setup()'tan dönen nesnenin click/type gibi metotları her zaman await edilmelidir. vi.fn() ile oluşturulan sahte fonksiyonlar, bir callback prop'un doğru argümanlarla çağrıldığını doğrulamak için kullanılır. Zamanla değişen (asenkron) DOM güncellemeleri, getByText yerine findByText/waitFor ile test edilir.

Terimler Sözlüğü

user-event — Kullanıcı etkileşimlerini (tıklama, yazma), gerçek tarayıcı davranışına yakın şekilde simüle eden kütüphane.

Mock Fonksiyonvi.fn() ile oluşturulan, gerçek bir fonksiyonun yerine geçip çağrılma bilgisini (argümanlar, sayı) kaydeden sahte fonksiyon.

Asenkron SorgufindBy* gibi, elemanın DOM'da görünmesini bir süre BEKLEYEN sorgu türü.

Pratik Proje

Bu kategoride (Component Testing, User Interaction Testing) öğrendiğimiz kavramları bir arada kullanan, gerçek ve çalıştırılabilir bir örnek proje var: Testing Demo -- önceki kategorilerden tanıdık, arama yapılabilen bir kurs listesi ve bir kayıt formu, ama bu kez odak uygulamanın kendisinden çok onu doğrulayan gerçek Vitest testlerinde.

Proje; SearchBar, CourseList ve EnrollForm component'lerinin her birini kendi .test.jsx dosyasında (getByLabelText+userEvent.type, getByText/queryByText ile filtreleme, vi.fn()+findByText ile form gönderimi) AYRI AYRI test etmeyi, ve bunların hepsini App içinde lifting state up ile bağlayan bütünü tek bir integration testiyle (App.test.jsx) doğrulamayı gösteriyor. Bilgisayarına indirip çalıştırabilir, testleri satır satır inceleyebilirsin:

git clone https://github.com/cdurgun/react-course-projects.git
cd react-course-projects
npm install
cd projects/testing
npm test

react-course-projects deposu npm workspaces kullanır -- npm install yalnızca bir kez, depo kökünde çalıştırılır ve tüm proje klasörleri ortak bağımlılıkları paylaşır (her klasörde ayrı bir node_modules oluşmaz). Kök dizinde npm install'ı daha önce çalıştırdıysan, doğrudan cd react-course-projects/projects/testing yapıp npm test demen yeterli.