Məzmuna keç
Educora
İrəli22 dəq26 / 27

JavaScript-də testləmə

Kodunun işlədiyini sübut et: Vitest və Jest ilə modul testləri, `describe`/`it`/`expect`, əsas müqayisə metodları, asinxron kodun və xətaların testi, mock və spy-lar, örtük (coverage) nə deməkdir.

Özünü yoxla
Bu dərsdə öyrənəcəksən
  • describe, it və expect ilə modul testləri yazmaq və uyğun müqayisə metodunu (toBe, toEqual, toThrow) seçmək
  • Asinxron kodu və xətaları test etmək, kodu mock, spy və saxta taymerlərlə kənar dünyadan ayırmaq
  • Yaxşı təcrübələrə əməl etmək: Arrange–Act–Assert, test piramidası, örtük və TDD

Endirim funksiyasındakı səhvi düzəldirsən — və ödəniş səhifəsi başqa yerdə sınır. Hər dəyişiklikdən sonra saytı əl ilə yoxlamaq saatlar aparır. Avtomatik testlər isə yüzlərlə halı bir saniyədə, hər dəfə faylı saxlayanda yoxlayır. JavaScript üçün ən populyar test alətləri Vitest və Jest-dir; onların API-ləri demək olar ki, eynidir, ona görə bu dərsdə öyrəndiklərin hər ikisində işləyəcək.

İlk modul testi

Modul testi (unit test) kiçik bir hissəni — adətən bir funksiyanı — təcrid olunmuş halda yoxlayır. Test faylı kodun yanında durur: price.js üçün price.test.js. describe testləri qruplaşdırır, it (və ya test) bir davranışı təsvir edir, expect(qiymət).toBe(gözlənilən) isə yoxlama aparır. npx vitest testləri işə salır və faylları izləyir.

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);
  });
});
Gözlənilən nəticə
 ✓ price.test.js (3 tests) 3ms

 Test Files  1 passed (1)
      Tests  3 passed (3)
Vitest-in qısaldılmış çıxışı; müddət hər dəfə fərqli olur. Test uğursuz olanda alət gözlənilən və alınan qiyməti yan-yana göstərir. toThrow üçün çağırışı ox funksiyasına sarmaq lazımdır — yoxsa xəta expect-dən əvvəl atılar.

expect necə işləyir

Test aləti sehrli deyil: it funksiyanı try...catch içində çağırır, expect isə şərt ödənməyəndə xəta atır. Aşağıdakı mini versiya bunu göstərir və iki vacib fərqi üzə çıxarır: toBe Object.is ilə müqayisə edir (eyni istinad), toEqual isə qiymətləri müqayisə edir; kəsr ədədləri isə birbaşa bərabərliklə yoxlamaq təhlükəlidir.

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'));
▸ Gözlənilən nəticə
✓ 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
Real alətlərdə kəsrlər üçün toBeCloseTo(0.3) var. Bizim toEqual sadələşdirilib — Vitest və Jest dərin müqayisəni daha diqqətlə aparır.
Müqayisə metoduNəyi yoxlayır
toBe(x)Object.is ilə bərabərlik: primitivlər, eyni istinad
toEqual(x)obyekt və massivlərin dərin bərabərliyi
toBeCloseTo(x)kəsr ədədlərin təxmini bərabərliyi
toContain(x), toHaveLength(n)massivdə element və ya sətirdə hissə; uzunluq
toThrow(Type)funksiyanın xəta atması
resolves / rejectsPromise-in nəticəsi və ya xətası
.notistənilən yoxlamanı inkar edir: expect(x).not.toBe(0)

Asinxron kod, mock və spy-lar

Asinxron test sadəcə async funksiyadır: await ilə nəticəni gözlə və ya await expect(promise).rejects.toThrow() yaz. Modul testi real e-poçt göndərməməli, real serverə müraciət etməməli və real vaxtı gözləməməlidir. Bunun üçün mock (saxta funksiya, vi.fn()), spy (mövcud metodun çağırışlarını izləyən, vi.spyOn) və saxta taymerlər (vi.useFakeTimers()) var. Ən sadə yol — asılılığı parametr kimi ötürmək: onda testdə real funksiyanın yerinə mock verirsən.

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');
  });
});
Gözlənilən nəticə
 ✓ notify.test.js (2 tests) 4ms

 Test Files  1 passed (1)
      Tests  2 passed (2)
Jest-də eyni kod vi əvəzinə jest ilə yazılır: jest.fn(), jest.spyOn(), jest.useFakeTimers().

Yaxşı testlərin qaydaları

  • Arrange–Act–Assert: hazırla, icra et, yoxla — hər test bu üç hissədən ibarətdir.
  • Bir test — bir davranış, adı isə nəyin gözlənildiyini deyir: «rejects a percent above 100».
  • Daxili quruluşu yox, davranışı test et — onda kodu yenidən yazanda testlər sınmır.
  • Test piramidası: çoxlu sürətli modul testləri, daha az inteqrasiya testləri, bir neçə uçdan-uca (end-to-end) test.
Tapşırıq

Testlər qırmızıdır: isLeapYear-də səhv var. Testlərə toxunmadan funksiyanı düzəlt ki, dörd test yaşıl olsun. Qayda: il 4-ə bölünürsə, uzun ildir, amma 100-ə bölünən illər yalnız 400-ə də bölünəndə uzun ildir.

Tapşırıq · 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'));
▸ Gözlənilən nəticə
✓ 2024 is a leap year
✓ 2023 is not a leap year
✓ 1900 is not a leap year
✓ 2000 is a leap year
Tapşırıq

Öz spy-ını yaz: createSpy() çağırılanda arqumentlərini spy.calls massivinə yazan funksiya qaytarsın. Sonra testin göstərdiyi kimi, notifyAll yalnız abunəçilərə məktub göndərməlidir — iki çağırış və ikinci çağırışın arqumentləri çap olunur.

Tapşırıq · 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]);
▸ Gözlənilən nəticə
calls: 2
[ 'leyla@mail.az', 'New lesson is out!' ]

Əsas fikirlər

  • Modul testi bir kiçik davranışı yoxlayır: describe qruplaşdırır, it təsvir edir, expect(...).matcher() yoxlayır.
  • toBe Object.is ilə, toEqual qiymətlərlə müqayisə edir; kəsrlər üçün toBeCloseTo, xətalar üçün toThrow işlət.
  • Asinxron testlərdə await və ya resolves/rejects; mock, spy və saxta taymerlər kodu şəbəkədən, vaxtdan və təsadüfdən ayırır.
  • Arrange–Act–Assert, bir testdə bir davranış, müstəqil testlər; çoxlu modul, az inteqrasiya və uçdan-uca testlər.
  • Örtük yalnız hansı sətirlərin işlədiyini göstərir; TDD: qırmızı → yaşıl → təmizlə.

Özünü yoxla

10 sual. Hər düzgün cavab XP qazandırır.

1 / 10
expect([1, 2]).toBe([1, 2]) nə ilə nəticələnir?