İçeriğe geç
Educora
İleri22 dk26 / 27

JavaScript'i test etmek

Kodunun çalıştığını kanıtla: Vitest ve Jest ile birim testleri, `describe`/`it`/`expect`, temel eşleştiriciler, asenkron kodu ve hataları test etmek, mock ve spy'lar ve kapsamın (coverage) gerçekte ne anlama geldiği.

Kendini test et
Bu derste öğreneceklerin
  • describe, it ve expect ile birim testleri yazmak ve doğru eşleştiriciyi (toBe, toEqual, toThrow) seçmek
  • Asenkron kodu ve hataları test etmek, kodu mock, spy ve sahte zamanlayıcılarla dış dünyadan yalıtmak
  • İyi uygulamalara uymak: Arrange–Act–Assert, test piramidi, kapsam ve TDD

İndirim fonksiyonundaki bir hatayı düzeltiyorsun ve ödeme sayfası başka bir yerde bozuluyor. Her değişiklikten sonra siteyi elle denemek saatler sürer. Otomatik testler ise her dosya kaydedişinde yüzlerce durumu bir saniyede kontrol eder. JavaScript için en popüler test çalıştırıcıları Vitest ve Jest'tir; API'leri neredeyse aynıdır, bu yüzden bu derste öğrendiklerin ikisinde de çalışır.

İlk birim testi

Bir birim testi (unit test), küçük bir parçayı (genellikle tek bir fonksiyonu) yalıtılmış olarak kontrol eder. Test dosyası kodun yanında durur: price.js için price.test.js. describe testleri gruplar, it (ya da test) tek bir davranışı tarif eder, expect(değer).toBe(beklenen) ise kontrolü yapar. npx vitest testleri çalıştırır ve dosyaları izlemeye devam eder.

JavaScript
// price.js
export function applyDiscount(price, percent) {
  if (percent < 0 || percent > 100) throw new RangeError('Percent must be 0-100');
  return Math.round(price * (100 - percent)) / 100;
}

// price.test.js
import { describe, it, expect } from 'vitest';
import { applyDiscount } from './price.js';

describe('applyDiscount', () => {
  it('reduces the price by the given percent', () => {
    expect(applyDiscount(200, 10)).toBe(180);
  });

  it('rounds to cents', () => {
    expect(applyDiscount(9.99, 15)).toBe(8.49);
  });

  it('rejects a percent above 100', () => {
    expect(() => applyDiscount(50, 120)).toThrow(RangeError);
  });
});
Beklenen çıktı
 ✓ price.test.js (3 tests) 3ms

 Test Files  1 passed (1)
      Tests  3 passed (3)
Vitest'in kısaltılmış çıktısı; süre her çalıştırmada farklıdır. Bir test başarısız olunca araç beklenen ve alınan değerleri yan yana gösterir. toThrow için çağrıyı bir ok fonksiyonuna sarmalısın; yoksa hata expect çalışmadan önce fırlatılır.

expect nasıl çalışır

Bir test çalıştırıcısı sihir değildir: it fonksiyonunu try...catch içinde çağırır, expect ise koşul sağlanmazsa bir hata fırlatır. Aşağıdaki mini sürüm bunu gösterir ve iki önemli farkı ortaya çıkarır: toBe Object.is ile (aynı referans), toEqual ise değerlere göre karşılaştırır; ondalık sayıları kesin eşitlikle kontrol etmek ise risklidir.

JavaScript
const results = [];
function it(name, fn) {
  try {
    fn();
    results.push(`✓ ${name}`);
  } catch (error) {
    results.push(`✗ ${name}: ${error.message}`);
  }
}
function expect(actual) {
  const show = (v) => JSON.stringify(v);
  return {
    toBe(expected) {
      if (!Object.is(actual, expected)) throw new Error(`expected ${show(expected)}, got ${show(actual)}`);
    },
    toEqual(expected) {
      if (show(actual) !== show(expected)) throw new Error(`expected ${show(expected)}, got ${show(actual)}`);
    },
  };
}

it('adds numbers', () => expect(2 + 3).toBe(5));
it('compares arrays by value', () => expect([1, 2]).toEqual([1, 2]));
it('toBe needs the same array', () => expect([1, 2]).toBe([1, 2]));
it('decimals are tricky', () => expect(0.1 + 0.2).toBe(0.3));
console.log(results.join('\n'));
▸ Beklenen çıktı
✓ adds numbers
✓ compares arrays by value
✗ toBe needs the same array: expected [1,2], got [1,2]
✗ decimals are tricky: expected 0.3, got 0.30000000000000004
Gerçek araçlarda ondalıklar için toBeCloseTo(0.3) vardır. Bizim toEqual'ımız basitleştirilmiştir; Vitest ve Jest derin karşılaştırmayı daha dikkatli yapar.
EşleştiriciNeyi kontrol eder
toBe(x)Object.is ile eşitlik: ilkel değerler, aynı referans
toEqual(x)nesnelerin ve dizilerin derin eşitliği
toBeCloseTo(x)ondalık sayıların yaklaşık eşitliği
toContain(x), toHaveLength(n)dizide bir eleman ya da string'de bir parça; uzunluk
toThrow(Type)fonksiyonun hata fırlatması
resolves / rejectsbir Promise'in sonucu ya da hatası
.notherhangi bir kontrolü olumsuzlar: expect(x).not.toBe(0)

Asenkron kod, mock ve spy'lar

Asenkron bir test yalnızca bir async fonksiyondur: sonucu await ile bekle ya da await expect(promise).rejects.toThrow() yaz. Bir birim testi gerçek e-posta göndermemeli, gerçek bir sunucuya gitmemeli ve gerçek zamanı beklememelidir. Bunun için mock'lar (sahte fonksiyonlar, vi.fn()), spy'lar (var olan bir metodun çağrılarını izler, vi.spyOn) ve sahte zamanlayıcılar (vi.useFakeTimers()) vardır. En basit yol, bağımlılığı parametre olarak vermektir: böylece testte gerçek fonksiyon yerine bir mock verirsin.

JavaScript
// notify.js
export async function notifyUser(user, sendEmail) {
  return sendEmail(user.email, `Welcome, ${user.name}!`);
}

// notify.test.js
import { describe, it, expect, vi } from 'vitest';
import { notifyUser } from './notify.js';

describe('notifyUser', () => {
  it('sends one welcome email', async () => {
    const sendEmail = vi.fn().mockResolvedValue({ ok: true });

    await notifyUser({ email: 'leyla@mail.az', name: 'Leyla' }, sendEmail);

    expect(sendEmail).toHaveBeenCalledTimes(1);
    expect(sendEmail).toHaveBeenCalledWith('leyla@mail.az', 'Welcome, Leyla!');
  });

  it('fails when the mail service is down', async () => {
    const sendEmail = vi.fn().mockRejectedValue(new Error('SMTP down'));
    await expect(notifyUser({ email: 'a@mail.az', name: 'Elvin' }, sendEmail)).rejects.toThrow('SMTP down');
  });
});
Beklenen çıktı
 ✓ notify.test.js (2 tests) 4ms

 Test Files  1 passed (1)
      Tests  2 passed (2)
Jest'te aynı kod vi yerine jest ile yazılır: jest.fn(), jest.spyOn(), jest.useFakeTimers().

İyi testlerin kuralları

  • Arrange–Act–Assert: hazırla, çalıştır, kontrol et; her test bu üç bölümden oluşur.
  • Bir test, bir davranış; adı da neyin beklendiğini söyler: “rejects a percent above 100”.
  • İç ayrıntıları değil davranışı test et; böylece testler yeniden düzenlemeden sağ çıkar.
  • Test piramidi: çok sayıda hızlı birim testi, daha az entegrasyon testi ve birkaç uçtan uca (end-to-end) test.
Alıştırma

Testler kırmızı: isLeapYear'da bir hata var. Testlere dokunmadan fonksiyonu düzelt ki dört testin hepsi yeşile dönsün. Kural: 4'e bölünen yıl artık yıldır, ama 100'e bölünen yıllar yalnızca 400'e de bölünüyorsa artık yıldır.

Alıştırma · JavaScript
function isLeapYear(year) {
  return year % 4 === 0;
}

const results = [];
function it(name, fn) {
  try {
    fn();
    results.push(`✓ ${name}`);
  } catch (error) {
    results.push(`✗ ${name}: ${error.message}`);
  }
}
const expect = (actual) => ({
  toBe(expected) {
    if (actual !== expected) throw new Error(`expected ${expected}, got ${actual}`);
  },
});

it('2024 is a leap year', () => expect(isLeapYear(2024)).toBe(true));
it('2023 is not a leap year', () => expect(isLeapYear(2023)).toBe(false));
it('1900 is not a leap year', () => expect(isLeapYear(1900)).toBe(false));
it('2000 is a leap year', () => expect(isLeapYear(2000)).toBe(true));
console.log(results.join('\n'));
▸ Beklenen çıktı
✓ 2024 is a leap year
✓ 2023 is not a leap year
✓ 1900 is not a leap year
✓ 2000 is a leap year
Alıştırma

Kendi spy'ını yaz: createSpy(), her çağrıldığında argümanlarını spy.calls dizisine kaydeden bir fonksiyon döndürsün. Kontrolün gösterdiği gibi notifyAll yalnızca abonelere e-posta göndermeli; çağrı sayısı ve ikinci çağrının argümanları yazdırılır.

Alıştırma · JavaScript
function createSpy() {
  const spy = (...args) => {
    // record args in spy.calls
  };
  spy.calls = [];
  return spy;
}

function notifyAll(users, send) {
  for (const user of users) {
    if (user.subscribed) send(user.email, 'New lesson is out!');
  }
}

const send = createSpy();
notifyAll([
  { email: 'aysel@mail.az', subscribed: true },
  { email: 'murad@mail.az', subscribed: false },
  { email: 'leyla@mail.az', subscribed: true },
], send);

console.log('calls:', send.calls.length);
console.log(send.calls[1]);
▸ Beklenen çıktı
calls: 2
[ 'leyla@mail.az', 'New lesson is out!' ]

Önemli noktalar

  • Bir birim testi küçük bir davranışı kontrol eder: describe gruplar, it tarif eder, expect(...).matcher() kontrol eder.
  • toBe Object.is ile, toEqual değere göre karşılaştırır; ondalıklar için toBeCloseTo, hatalar için toThrow kullan.
  • Asenkron testlerde await ya da resolves/rejects; mock, spy ve sahte zamanlayıcılar kodu ağdan, zamandan ve rastlantıdan yalıtır.
  • Arrange–Act–Assert, test başına bir davranış, bağımsız testler; çok sayıda birim, daha az entegrasyon ve uçtan uca test.
  • Kapsam yalnızca hangi satırların çalıştığını gösterir; TDD, kırmızı → yeşil → düzenle demektir.

Kendini test et

10 soru. Her doğru cevap XP kazandırır.

1 / 10
expect([1, 2]).toBe([1, 2]) neyle sonuçlanır?