IlmHamroh
JavaScript Full-stack/12-qism. JavaScript: ilg'or mavzular va kod sifati35/44-dars24 daqiqa
Mundarija (36)

Birinchi avtomatik test: node:test va assert — kodni o'zi tekshiradigan kod

Qisqacha: Avtomatik test — kodingizni tekshiradigan kichik kod. assert.equal(haqiqiy, kutilgan) ikki qiymat teng bo'lmasa xato tashlaydi. test("nomi", () => { … }) tekshiruvlarga nom beradi, describe ularni guruhlaydi. Terminalda node --test loyihadagi barcha *.test.js fayllarni topib ishga tushiradi va natijani ✔/✖ bilan chiqaradi. Hammasi Node.js ichida bor — hech narsa o'rnatish shart emas.

Bu darsda

  • Avtomatik test nima ekanini va qo'lda tekshirishdan nimasi bilan yaxshiligini tushuntira olasiz.
  • node:assert/strict bilan equal, deepEqual, throws tekshiruvlarini yozasiz va yiqilgan tekshiruv xabarini o'qiysiz.
  • node:test da test va describe bilan test fayl yozib, node --test bilan ishga tushirasiz.
  • Chegaraviy holatlarni topib, ularni "qizil → yashil" sikli bilan testga aylantirasiz.
  • vazifalar ning tekshiruv skriptlarini haqiqiy testlarga aylantirasiz.

Oldin bilishingiz kerak: Toza kod: funksiyalar, izohlar va tuzilma, Xato strategiyasi, CommonJS va ESM bilan o'zaro ishlash, npm'ga birinchi qadam.

1. Nega bu kerak?

O'tgan ikki darsda vazifalar ning nomlari va funksiyalarini tozaladik. Har safar xulq o'zgarmaganini qanday isbotladik? Skriptlarni ishga tushirib, chiqishni faylga yozdik, keyin git diff bilan eski fayl bilan solishtirdik. Har o'zgarishdan keyin — to'rtta buyruq, ikki fayl, ko'z bilan qarash.

Bu ishladi, lekin uchta muammosi bor. Birinchisi — sekin: kichik o'zgarish uchun ham hammasini qo'lda qilasiz. Ikkinchisi — ishonchsiz: ✅ qatorlarini ko'z bilan o'qiysiz, bittasi ❌ bo'lsa, o'tkazib yuborishingiz mumkin. Uchinchisi — skriptdagi tekshiruvlar bir-biriga bog'liq: bitta umumiy ro'yxat ketma-ket o'zgartiriladi, beshinchi qator yiqilsa, oltinchisi ham noto'g'ri bo'ladi.

Debugging metodologiyasi darsida bir gap aytgan edik: avtomatik test — kodni siz o'rningizga tekshiradigan kichik dastur. Bugun uni yozamiz. Siz bir marta "bu funksiya shunday ishlashi kerak" deb yozasiz, keyin bitta buyruq har safar hamma holatni bir soniyadan kamroq vaqtda qayta tekshiradi. Bu kran ostidagi o'lchov idishiga o'xshaydi: suv har safar bir xil chiziqqa yetishi kerak — yetmasa, darhol ko'rinadi.

2. assert — birinchi tekshiruv

2.1 Tekshiruv — bitta qator

O'tgan darsda «Bahor» chekidan calculateTotal ("jamini hisobla") funksiyasini ajratgan edik. U toza funksiya: buyurtma beriladi, summa qaytadi. Uni tekshirish uchun Node.js'ning ichki moduli bor — node:assert/strict:

js
import assert from "node:assert/strict";

function calculateTotal(order) {
  return order.reduce(
    (total, item) => total + item.price * item.qty,
    0,
  );
}

assert.equal(calculateTotal([{ price: 5000, qty: 3 }]), 15000);
assert.equal(calculateTotal([]), 0);
assert.equal(calculateTotal([{ price: 35000, qty: 2 }]), 70000);
console.log("Hammasi joyida");

Konsolda:

text
Hammasi joyida

assert — inglizcha "qat'iy ta'kidlamoq". assert.equal(haqiqiy, kutilgan) aytadi: "men ishonamanki, bu ikki qiymat teng". Teng bo'lsa — hech narsa bo'lmaydi, kod davom etadi. Teng bo'lmasa — xato tashlaydi va dastur to'xtaydi. Uchta tekshiruv o'tdi, shuning uchun oxirgi qatorga yetib keldik.

Tartibga e'tibor bering: birinchi — haqiqiy qiymat (funksiya nima qaytardi), ikkinchi — kutilgan (nima bo'lishi kerak). Xato xabari shu tartibga tayanadi.

Bu blok faqat Node.js'da ishlaydi — node: bilan boshlangan modullar brauzerda yo'q. Shuning uchun uni o'zingizning kompyuteringizda, .mjs faylga saqlab, node fayl.mjs bilan ishga tushiring.

2.2 Tekshiruv yiqilganda

Endi oxirgi tekshiruvda ataylab adashamiz — 70 000 o'rniga 75 000 yozamiz:

js
import assert from "node:assert/strict";

function calculateTotal(order) {
  return order.reduce(
    (total, item) => total + item.price * item.qty,
    0,
  );
}

assert.equal(calculateTotal([{ price: 35000, qty: 2 }]), 75000);
console.log("Hammasi joyida");

Terminalda (Node 24; fayl yo'li va pastki qatorlar qisqartirildi):

text
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:

70000 !== 75000

    at file:///…/jami.mjs:10:8
    ...
  code: 'ERR_ASSERTION',
  actual: 70000,
  expected: 75000,
  operator: 'strictEqual',

O'qiymiz. AssertionError — "ta'kid xatosi": ta'kidlagan narsangiz rost chiqmadi. "Expected values to be strictly equal" — "qiymatlar qat'iy teng bo'lishi kutilgan edi". Keyin aniq farq: 70000 !== 75000 — haqiqiy 70 000, kutilgan 75 000. at file:///…/jami.mjs:10:8 — qaysi faylning qaysi qatori. "Hammasi joyida" chiqmadi: dastur birinchi yiqilgan tekshiruvda to'xtadi.

Bu yerda funksiya to'g'ri, test xato edi: 35 000 × 2 — 70 000. Test ham kod, u ham adashishi mumkin. Test yiqilganda birinchi savol: "Kod xatomi yoki kutganim?"

2.3 equal va deepEqual

assert.equal qiymatlarni === ga o'xshab solishtiradi (aniqrog'i — Object.is bilan). Sonlar va satrlar uchun bu to'g'ri. Massiv va obyektlarda esa tuzoq bor:

js
import assert from "node:assert/strict";

assert.equal([1, 2], [1, 2]);

Terminalda:

text
AssertionError [ERR_ASSERTION]: Values have same structure but are not reference-equal:

[
  1,
  2
]

"Tuzilishi bir xil, lekin havolasi boshqa" — sayoz va chuqur nusxalash darsidagi gap: ikkita alohida massiv === bilan teng emas. Ichidagi qiymatlarni solishtirish uchun assert.deepEqual:

js
import assert from "node:assert/strict";

assert.deepEqual([1, 2], [1, 2]);
assert.deepEqual(
  { name: "Osh", price: 35000 },
  { price: 35000, name: "Osh" },
);
console.log("Ichi bir xil");

Konsolda:

text
Ichi bir xil

Obyekt kalitlarining tartibi muhim emas — qiymatlari teng bo'lsa yetarli. Turi esa muhim:

js
import assert from "node:assert/strict";

assert.deepEqual(
  { name: "Ali", age: 20 },
  { name: "Ali", age: "20" },
);

Terminalda:

text
AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
+ actual - expected

  {
+   age: 20,
-   age: '20',
    name: 'Ali'
  }

+ actual — haqiqiy qiymat (+ belgili qator), - expected — kutilgan (- belgili qator). Farq aynan qaysi kalitda ekanini ko'rsatadi: son 20 va satr "20". Kalitlar alifbo tartibida chiqadi: age — name dan oldin. Modul nomidagi strict so'zi shuni bildiradi: 20 va "20" teng emas. Shuning uchun biz doim node:assert/strict ni import qilamiz — oddiy node:assert ning eski, "yumshoq" variantlari turni e'tiborsiz qoldiradi.

2.4 throws — xato kutilganda

Ba'zan to'g'ri xulq — xato tashlash. Xato strategiyasi darsida noto'g'ri kirishga RangeError tashlashni o'rgangan edik. Buni ham tekshirsa bo'ladi:

js
import assert from "node:assert/strict";

function applyDiscount(amount) {
  if (!Number.isFinite(amount) || amount < 0) {
    throw new RangeError(`Noto'g'ri summa: ${amount}`);
  }
  return amount; // chegirma hisobi bu misolda muhim emas
}

assert.throws(() => applyDiscount(-5000), RangeError);
assert.throws(() => applyDiscount(NaN), {
  name: "RangeError",
  message: "Noto'g'ri summa: NaN",
});
console.log("Xatolar to'g'ri tashlandi");

Konsolda:

text
Xatolar to'g'ri tashlandi

assert.throws ga funksiyaning o'zini emas, uni chaqiradigan arrow'ni beramiz: () => applyDiscount(-5000). Agar applyDiscount(-5000) ni to'g'ridan-to'g'ri yozsak, xato throws ishga tushmasidan oldin chiqib ketardi. Ikkinchi argument — kutilgan xato: klass (RangeError) yoki xususiyatlari yozilgan obyekt. Funksiya xato tashlamasa yoki boshqa xato tashlasa — throws o'zi yiqiladi.

Yana ikki foydali tekshiruv: assert.ok(qiymat) — qiymat truthy bo'lsin; assert.match(satr, /regex/) — satr naqshga mos kelsin (RegExp asoslari).

Tekshirib ko'ring: assert.equal({ price: 5000 }, { price: 5000 }) o'tadimi? assert.equal("5000", 5000) ni-chi?

Javob

Ikkalasi ham yiqiladi. Birinchisida — ikki alohida obyekt, havolasi boshqa; kerakli tekshiruv — deepEqual. Ikkinchisida — satr va son; strict rejimda turi ham solishtiriladi, "5000" !== 5000.

3. test — nomli tekshiruv

3.1 Birinchi test

Faqat assert bilan ishlashning kamchiligi: birinchi yiqilgan tekshiruvda hammasi to'xtaydi, qolganlari haqida hech narsa bilmaymiz. Ularning nomi ham yo'q. Node.js'ning ikkinchi ichki moduli — node:test — buni hal qiladi:

js
import { test } from "node:test";
import assert from "node:assert/strict";

function calculateTotal(order) {
  return order.reduce(
    (total, item) => total + item.price * item.qty,
    0,
  );
}

test("bitta taom", () => {
  assert.equal(calculateTotal([{ price: 35000, qty: 2 }]), 70000);
});

test("bo'sh buyurtma — 0", () => {
  assert.equal(calculateTotal([]), 0);
});

Faylni node fayl.mjs bilan ishga tushirsangiz (vaqtlar har safar biroz boshqacha bo'ladi):

text
✔ bitta taom (0.8813ms)
✔ bo'sh buyurtma — 0 (0.1963ms)
ℹ tests 2
ℹ suites 0
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 8.8223

test(nomi, funksiya) — bitta test: nom va uning ichidagi tekshiruvlar. ✔ — test o'tdi, qavs ichida — qancha vaqt oldi. Pastda hisobot: tests 2 — jami, pass 2 — o'tgani, fail 0 — yiqilgani. Qolgan qatorlarni keyinroq ko'ramiz.

Test nomi — bu hujjat. "bo'sh buyurtma — 0" o'qigan odam funksiya qanday ishlashi kerakligini bilib oladi. Nomni xulq haqidagi gap kabi yozing: "nima berilsa — nima bo'ladi".

3.2 describe — guruh

Bitta funksiyaning testlari ko'payganda, ularni describe bilan guruhlaymiz. Guruhni suite (to'plam) ham deyishadi — hisobotdagi suites qatori shuni sanaydi. Quyidagi misolda applyDiscount ataylab xato yozilgan — Sardor >= o'rniga > qo'ygan:

js
import { describe, test } from "node:test";
import assert from "node:assert/strict";

// 200 000 so'mdan boshlab — 10% chegirma
function applyDiscount(amount) {
  if (amount > 200000) {
    return amount * 0.9;
  }
  return amount;
}

describe("applyDiscount", () => {
  test("kichik buyurtma — chegirmasiz", () => {
    assert.equal(applyDiscount(70000), 70000);
  });

  test("chegara: aynan 200 000 — chegirma bor", () => {
    assert.equal(applyDiscount(200000), 180000);
  });

  test("katta buyurtma — 10% kam", () => {
    assert.equal(applyDiscount(300000), 270000);
  });
});

Terminalda (stack qatorlari qisqartirildi):

text
▶ applyDiscount
  ✔ kichik buyurtma — chegirmasiz (0.8507ms)
  ✖ chegara: aynan 200 000 — chegirma bor (1.3873ms)
  ✔ katta buyurtma — 10% kam (0.1963ms)
✖ applyDiscount (3.8968ms)
ℹ tests 3
ℹ suites 1
ℹ pass 2
ℹ fail 1
...

✖ failing tests:

test at b2.mjs:17:3
✖ chegara: aynan 200 000 — chegirma bor (1.3873ms)
  AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:

  200000 !== 180000

Uchta muhim narsa. Birinchisi — test yiqilsa ham, keyingi test ishladi: uchinchi test o'tganini ko'ryapmiz. Har test o'z "qutisi"da. Ikkinchisi — pastda yiqilgan testlar alohida ro'yxatda, nomi va qatori bilan. Uchinchisi — nom xatoni o'zi aytib turibdi: "aynan 200 000 da chegirma bor" — demak, chegara noto'g'ri.

describe ichidagi testlar uchun test o'rniga it ham yozish mumkin — bu bir xil funksiya, boshqa nomi. Ko'p loyihalarda describe("…", () => { it("…") }) ko'rasiz: inglizchada "describe … it should …" gap kabi o'qiladi.

4. Qizil → yashil

4.1 Chegaraviy holatlar

Yuqoridagi xato chegarada yashiringan edi: 199 999 va 200 001 da > ham, >= ham bir xil ishlaydi — faqat aynan 200 000 da farq qiladi. Xatolar aynan shunday joylarni yaxshi ko'radi. Ularni chegaraviy holatlar (edge cases) deyishadi.

Test yozishdan oldin o'zingizga savol bering:

  • Chegara qayerda? Undan bitta kam, aynan o'zi, bitta ko'p.
  • Bo'sh kirish: [], "", 0.
  • Noto'g'ri kirish: manfiy son, NaN, undefined.
  • Maxsus belgilar: o'zbekcha apostrof, emoji, kirill harflari (Unicode regex darsidagi teglar).

4.2 Sikl

Xatoni tuzatishning ishonchli tartibi uch qadamdan iborat:

  1. Qizil — xatoni ko'rsatadigan testni yozing va uning yiqilishini ko'ring.
  2. Yashil — kodni tuzating, test o'tsin.
  3. Toza — kerak bo'lsa kodni tartibga keltiring; testlar yashil qolsin.
flowchart LR
  Q["Qizil: test yozildi, yiqildi ✖"] --> Y["Yashil: kod tuzatildi, o'tdi ✔"]
  Y --> T["Toza: refaktor, testlar yashil"]
  T --> Q

Nega avval yiqilishini ko'rish kerak? Chunki hech qachon yiqilmaydigan test ham bo'ladi — masalan, assert yozish esdan chiqqan bo'lsa yoki noto'g'ri funksiya chaqirilgan bo'lsa. Qizilni ko'rgan test haqiqatan nimanidir tekshiradi.

Testni koddan oldin yozish va shu sikl bo'yicha ishlash uslubi TDD (test-driven development — testga asoslangan ishlab chiqish) deyiladi. Hozir uni to'liq qo'llash shart emas, lekin xatoni tuzatishda qoida oddiy: avval xatoni testga aylantiring, keyin tuzating. Shunda bu xato hech qachon jimgina qaytib kelmaydi.

Tekshirib ko'ring: «Bahor» 7:00 dan 23:00 gacha ishlaydi. isOpen(hour) ("ochiqmi") funksiyasi uchun qaysi soatlarni testga yozardingiz?

Javob

Chegaralar atrofi: 6 (yopiq), 7 (ochiq — ochilish), 22 (ochiq), 23 (yopiq — yopilish payti). Shuningdek, 0 yoki 12 kabi "oddiy" holatlar va noto'g'ri qiymatlar (−1, 25), agar funksiya ularni rad etishi kerak bo'lsa. Eng muhimlari — 7 va 23: < va <= xatosi aynan shu yerda chiqadi.

5. Loyihada: test fayllari va node --test

5.1 Kod — bir faylda, test — boshqasida

Haqiqiy loyihada funksiyalar o'z modulida yashaydi, testlar esa alohida faylda. «Bahor» hisob-kitobi uchun kichik loyiha:

text
bahor-hisob/
  package.json
  hisob.js
  hisob.test.js

hisob.js — tuzatilgan kod, endi eksport qilinadi (ESM export):

js
// hisob.js — «Bahor» buyurtma hisobi
export const DISCOUNT_THRESHOLD = 200000; // so'm
export const DISCOUNT_PERCENT = 10;

export function calculateTotal(order) {
  return order.reduce(
    (total, { price, qty }) => total + price * qty,
    0,
  );
}

export function applyDiscount(amount) {
  if (!Number.isFinite(amount) || amount < 0) {
    throw new RangeError(`Noto'g'ri summa: ${amount}`);
  }
  if (amount < DISCOUNT_THRESHOLD) {
    return amount;
  }
  return (amount * (100 - DISCOUNT_PERCENT)) / 100;
}

O'tgan darsdagi applyDiscount ga bitta himoya sharti qo'shildi: noto'g'ri summa — RangeError.

hisob.test.js — uning testlari. Nomi kod fayli bilan bir xil, faqat .test qo'shilgan:

js
// hisob.test.js — hisob.js testlari
import { describe, test } from "node:test";
import assert from "node:assert/strict";
import { calculateTotal, applyDiscount } from "./hisob.js";

describe("calculateTotal", () => {
  test("bir nechta taom", () => {
    const order = [
      { price: 35000, qty: 2 },
      { price: 5000, qty: 3 },
    ];
    assert.equal(calculateTotal(order), 85000);
  });

  test("bo'sh buyurtma — 0", () => {
    assert.equal(calculateTotal([]), 0);
  });
});

describe("applyDiscount", () => {
  test("chegaradan bitta kam — chegirmasiz", () => {
    assert.equal(applyDiscount(199999), 199999);
  });

  test("aynan chegara — 10% chegirma", () => {
    assert.equal(applyDiscount(200000), 180000);
  });

  test("manfiy summa — RangeError", () => {
    assert.throws(() => applyDiscount(-5000), RangeError);
  });

  test("son emas — xato matni bilan", () => {
    assert.throws(() => applyDiscount(NaN), {
      name: "RangeError",
      message: "Noto'g'ri summa: NaN",
    });
  });
});

package.json da — "type": "module" (ESM) va npm skripti:

json
{
  "name": "bahor-hisob",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "node --test"
  }
}

5.2 npm test

Terminalda loyiha papkasida npm test (bu npm run test ning qisqa shakli). Bizda (Node 24.21.0, npm 11.19.0) shunday chiqdi:

text
> test
> node --test

▶ calculateTotal
  ✔ bir nechta taom (1.709ms)
  ✔ bo'sh buyurtma — 0 (0.1953ms)
✔ calculateTotal (3.2373ms)
▶ applyDiscount
  ✔ chegaradan bitta kam — chegirmasiz (0.3375ms)
  ✔ aynan chegara — 10% chegirma (0.1543ms)
  ✔ manfiy summa — RangeError (0.5595ms)
  ✔ son emas — xato matni bilan (0.4966ms)
✔ applyDiscount (1.9386ms)
ℹ tests 6
ℹ suites 2
ℹ pass 6
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 157.9356

node --test — Node.js'ning ichki test yurgizuvchisi (test runner). U papkani o'zi aylanib, test fayllarini topadi va har birini alohida ishga tushiradi. Test topilmasa yoki bittasi yiqilsa, buyruq xato kodi bilan tugaydi — keyinchalik avtomatik tekshiruv tizimlari (CI) aynan shunga qaraydi.

5.3 Test fayllarini qanday topadi

node --test faqat nomi ma'lum shaklda bo'lgan fayllarni ishga tushiradi. Eng ko'p ishlatiladigani — *.test.js (yana *-test.js, *_test.js, test-*.js, test papkasi ichidagi fayllar va .mjs/.cjs variantlari). node_modules papkasi o'tkazib yuboriladi.

Bundan foydali xulosa: test bo'lmagan faylni ataylab boshqacha nomlash mumkin. Masalan, haqiqiy serverga so'rov yuboradigan sekin test api.tarmoq.js deb nomlansa, oddiy npm test uni ishga tushirmaydi. Uni alohida, internet bor paytda ishga tushirasiz. Vazifalar qadamida aynan shunday qilamiz.

5.4 Foydali imkoniyatlar

  • node --test --watch — fayl saqlanganda testlar o'zi qayta ishga tushadi. Kod yozayotganda terminalni ochiq qoldirasiz.
  • test.todo("…") — hali yozilmagan testning nomi, eslatma sifatida.
  • test.skip("…", …) — vaqtincha o'tkazib yuboriladigan test.
  • test.only("…", …) — faqat shu test (node --test --test-only bilan).

Hisobotdagi skipped va todo qatorlari shularni sanaydi:

js
import { test } from "node:test";

test.todo("katta buyurtmaga bepul choy");
test.skip("Click orqali to'lov", () => {});
test("oddiy", () => {});

Terminalda (vaqtlar har safar boshqacha; # TODO va # SKIP belgilari bilan):

text
✔ katta buyurtmaga bepul choy (1.0257ms) # TODO
﹣ Click orqali to'lov (0.2319ms) # SKIP
✔ oddiy (0.2218ms)
ℹ tests 3
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 1
ℹ todo 1
ℹ duration_ms 10.5168

skip ni unutib qoldirmang: o'tkazilgan test hech narsani himoya qilmaydi.

5.5 Har test — mustaqil

Testlar istalgan tartibda va alohida ishlashi kerak. Agar ikkinchi test birinchisi o'zgartirgan ro'yxatga tayansa, birinchisini o'chirganingizda ikkinchisi ham buziladi — sababi esa ko'rinmaydi. Qoida: har test o'z ma'lumotini o'zi yaratadi. Ko'p testga bir xil tayyorgarlik kerak bo'lsa — beforeEach(() => { … }): u har testdan oldin ishlaydi (afterEach — keyin).

Tekshirib ko'ring: bahor-hisob papkasiga tekshiruv.js nomli fayl qo'shdingiz, ichida test(...) bor. npm test uni ishga tushiradimi?

Javob

Yo'q. tekshiruv.js nomi test naqshlariga (*.test.js, test-*.js va boshqalar) mos kelmaydi. Uni tekshiruv.test.js deb nomlang yoki node --test tekshiruv.js deb faylni aniq ko'rsating.

6. Nimani test qilish oson va nimani qiyin

6.1 Toza funksiyalar — eng oson

calculateTotal — toza funksiya: kirish berasiz, chiqishni solishtirasiz. Uni tekshirish uchun brauzer ham, server ham kerak emas — FP g'oyalari va deklarativ uslub darsidagi "funksional yadro" aynan shu. Xuddi shunday — Intl.NumberFormat darsidagi somYoz (bu yerda inglizcha nomi — formatSom). O'sha darsda bitta tuzoq aytilgan edi: natijadagi guruh ajratuvchi — oddiy bo'sh joy emas, uzilmas bo'sh joy (U+00A0). Testda uni \u00a0 bilan yozamiz:

js
import { test } from "node:test";
import assert from "node:assert/strict";

const numberFormat = new Intl.NumberFormat("uz-UZ", {
  maximumFractionDigits: 0,
});

function formatSom(price) {
  const text = numberFormat
    .formatToParts(price)
    .map((p) => (p.type === "group" ? "\u00a0" : p.value))
    .join("");
  return `${text} so'm`;
}

test("formatSom: guruhlar uzilmas bo'sh joy bilan", () => {
  assert.equal(formatSom(35000), "35\u00a0000 so'm");
  assert.equal(formatSom(1250000), "1\u00a0250\u00a0000 so'm");
});

test("oddiy bo'sh joy bilan teng emas", () => {
  assert.notEqual(formatSom(35000), "35 000 so'm");
});

Terminalda ikkala test ham ✔ (bizda Node 24 da sinaldi). Ko'zga bir xil ko'rinadigan ikki satr teng emas — test buni aniq ushlaydi. Formatlovchining o'zini (Intl) emas, o'zimizning formatSom ni tekshiryapmiz: Intl ning natijasi brauzerga qarab farq qiladi, bizning funksiyamiz esa bir xil bo'lishi kerak.

6.2 Tashqi narsalarga bog'liq kod

Qiyini — funksiya o'zidan tashqaridagi narsaga tayanganda: joriy vaqt, internet, localStorage. Ularning javobi har safar boshqacha. Yechim — tashqi narsani tashqaridan berish:

  • Joriy vaqt — parametr: funksiya ichida new Date() ni o'zi olmaydi, soatni chaqiruvchi beradi: isOpen(hour). Testda istalgan soatni berasiz (Date asoslari darsida aytgan edik).
  • Tarmoq — fetch o'rniga soxta (mock) funksiya. U tarmoqqa bormaydi: oldindan tayyorlangan javobni qaytaradi va qanday chaqirilganini eslab qoladi. Buning uchun node:test da mock bor. Request va Response darsida "bog'liqlikni tashqaridan berish" degan g'oyani ko'rgan edik — testlar uning eng katta foydalanuvchisi.

mock ning to'liq imkoniyatlarini (taymerlarni "to'xtatish", spy) frontend testlash qismida Vitest bilan o'rganamiz (Vitest I) — u node:test ga juda o'xshaydi, faqat assert.equal(a, b) o'rniga expect(a).toBe(b) yoziladi. Hozir bilish shart emas: bugungi Vazifalar qadamida mock ning bitta, eng sodda ishlatilishini ko'rasiz.

7. Ko'p uchraydigan xatolar

7.1 Obyektni equal bilan solishtirish

Values have same structure but are not reference-equal — massiv yoki obyekt uchun deepEqual kerak. Xabarning o'zi shuni aytib turibdi: "tuzilishi bir xil".

7.2 Funksiyani throws ga chaqirib berish

assert.throws(applyDiscount(-5000), RangeError) — xato throws dan oldin chiqadi va test "kutilmagan xato" bilan yiqiladi. To'g'risi — arrow: () => applyDiscount(-5000).

7.3 Asinxron testda await yo'q

async funksiya tashlagan xato — rad etilgan Promise (Promise asoslari). Uni assert.rejects tekshiradi va unga albatta await kerak: await assert.rejects(api.ochir(999), { kod: "TOPILMADI" }). await siz test tekshiruv tugashini kutmay "o'tdi" deb chiqadi. Test funksiyasining o'zi ham async bo'lsin: test("…", async () => { … }).

7.4 Testlar bir-biriga bog'liq

Bitta umumiy o'zgaruvchini hamma test o'zgartiradi — tartib o'zgarsa, testlar tasodifan yiqiladi. Har test o'z ma'lumotini yaratsin yoki beforeEach ishlating.

7.5 Hech qachon yiqilmagan test

Testni yozdingiz — u darhol yashil. Kodni ataylab buzib ko'ring (>= ni > ga). Test qizarmasa — u hech narsani tekshirmayapti.

8. Mashqlar

1-mashq (oson): Tekshiruvni to'ldiring

O'zingiz to'ldiring. Massivni solishtirish uchun qaysi tekshiruv kerak?

assert.(["osh", "manti"], ["osh", "manti"]);

Endi o'tgan darsdagi bookingError uchun uchta assert yozing: 25 kishi, 4 kishi soat 19 da, 4 kishi soat 23 da.

Yechim
js
import assert from "node:assert/strict";

const MAX_GUESTS = 20;
const BOOKING_START = 10; // soat
const BOOKING_END = 22;

function bookingError(booking) {
  if (booking.guests < 1) {
    return "Kamida 1 kishi";
  }
  if (booking.guests > MAX_GUESTS) {
    return `Ko'pi bilan ${MAX_GUESTS} kishi`;
  }
  if (booking.hour < BOOKING_START || booking.hour > BOOKING_END) {
    return "Bron 10:00–22:00 oralig'ida";
  }
  return null;
}

assert.equal(
  bookingError({ guests: 25, hour: 12 }),
  "Ko'pi bilan 20 kishi",
);
assert.equal(bookingError({ guests: 4, hour: 19 }), null);
assert.equal(
  bookingError({ guests: 4, hour: 23 }),
  "Bron 10:00–22:00 oralig'ida",
);
console.log("Uchala tekshiruv o'tdi");

Konsolda:

text
Uchala tekshiruv o'tdi

null ham oddiy qiymat — equal bilan solishtiriladi. 23 — chegaradan bitta ko'p, shuning uchun aynan uni tanladik.

2-mashq (o'rta): isOpen uchun testlar

«Bahor» soat 7:00 dan 23:00 gacha ochiq. isOpen(hour) funksiyasini yozing: hour — soat, 0 dan 23 gacha butun son. Ochilish va yopilish soatlariga nom bering. Keyin describe ichida chegaraviy testlar yozing: 6, 7, 22, 23.

Yechim
js
import { describe, test } from "node:test";
import assert from "node:assert/strict";

const OPENING_HOUR = 7;
const CLOSING_HOUR = 23;

function isOpen(hour) {
  return hour >= OPENING_HOUR && hour < CLOSING_HOUR;
}

describe("isOpen", () => {
  test("6:00 — hali yopiq", () => {
    assert.equal(isOpen(6), false);
  });

  test("7:00 — ochildi", () => {
    assert.equal(isOpen(7), true);
  });

  test("22:00 — ochiq", () => {
    assert.equal(isOpen(22), true);
  });

  test("23:00 — yopildi", () => {
    assert.equal(isOpen(23), false);
  });
});

Ishga tushirsangiz — to'rttasi ham ✔, tests 4, suites 1. Endi qizilni ko'ring: < ni <= ga almashtiring — "23:00 — yopildi" testi ✖ bo'ladi (true !== false). Demak, test haqiqatan chegarani himoya qiladi.

3-mashq (qiyin): Avval qizil

Jasur aka yangi qoida aytdi: "10 tadan ortiq ko'k choy buyurtma qilinsa, choy 4 000 so'mdan". Avval test yozing va uning yiqilishini ko'ring, keyin teaCost(qty) ("choy narxi": qty ta choy uchun jami) ni tuzating. Boshlang'ich kod — eski qoida: choy doim 5 000 so'm.

js
const TEA_PRICE = 5000;

function teaCost(qty) {
  return qty * TEA_PRICE;
}
Yechim

1-qadam (qizil): testlar yozildi, eski kod bilan "11 ta — arzon" yiqiladi (55000 !== 44000).

2-qadam (yashil):

js
import { describe, test } from "node:test";
import assert from "node:assert/strict";

const TEA_PRICE = 5000;
const BULK_PRICE = 4000; // ulgurji narx
const BULK_FROM = 10; // shundan KO'P bo'lsa arzon

function teaCost(qty) {
  const price = qty > BULK_FROM ? BULK_PRICE : TEA_PRICE;
  return qty * price;
}

describe("teaCost", () => {
  test("10 ta — oddiy narx", () => {
    assert.equal(teaCost(10), 50000);
  });

  test("11 ta — har biri 4 000", () => {
    assert.equal(teaCost(11), 44000);
  });

  test("0 ta — 0", () => {
    assert.equal(teaCost(0), 0);
  });
});

E'tibor bering: 11 ta choy (44 000) 10 tadan (50 000) arzon chiqyapti. Test buni ochiq ko'rsatdi — bu kod xatosi emas, qoidaning g'alati natijasi. Uni Jasur akaga ko'rsatish kerak. Testlar ko'pincha talabdagi bo'shliqni ham topadi.

4-mashq: Vazifalar qadami — tekshiruv skriptlari testlarga aylanadi

Branch: chore/node-test. Ilova kodi (assets/js) o'zgarmaydi — faqat tekshiruv/ papkasi va package.json.

Nima o'zgaradi.

  • tekshiruv/{royxat,paket,saqlash,marshrut}.js (/ chiqaruvchi skriptlar) → *.test.js fayllar: describe/test va assert.equal/deepEqual/throws/match/rejects. Eski skriptlar o'chiriladi. xotira.js (Node'da localStorage o'rinbosari) qoladi.
  • tekshiruv/api.js (haqiqiy API'ga boradi) → tekshiruv/api.tarmoq.js. Nomi *.test.js emas — npm test uni ishga tushirmaydi, internetsiz ham yashil.
  • package.json ga ikki skript: "test": "node --test" va "test:tarmoq": "node --test tekshiruv/api.tarmoq.js". README dagi "Tekshirish" bo'limi yangilanadi.

Oldin va keyin. Eski skriptdagi yordamchi va bitta tekshiruv:

js
// Oldin (tekshiruv/royxat.js)
function tekshir(nomi, haqiqiy, kutilgan) {
  const belgi = haqiqiy === kutilgan ? "✅" : "❌";
  console.log(`${belgi} ${nomi}: ${haqiqiy}`);
}

const royxat = VazifalarRoyxati.dan(boshlangichVazifalar);
tekshir("yangi id", royxat.qosh("Sabzi olish #bozor").id, 4);
js
// Keyin (tekshiruv/royxat.test.js)
// Har test o'z ro'yxati bilan: testlar bir-biriga bog'liq bo'lmasin
function yangiRoyxat() {
  return VazifalarRoyxati.dan(boshlangichVazifalar);
}

test("yangi id — eng kattasidan keyingisi", () => {
  assert.equal(yangiRoyxat().qosh("Sabzi olish").id, 4);
});

test("o'chirilgan id qayta berilmaydi", () => {
  const royxat = yangiRoyxat();
  royxat.qosh("Sabzi olish");
  royxat.ochir(4);
  assert.equal(royxat.qosh("Choy damlash").id, 5);
});

Asosiy farq: eski skript bitta umumiy royxat ni ketma-ket o'zgartirardi — tekshiruvlar bir-biriga bog'liq edi. Endi har test yangiRoyxat() bilan o'z ro'yxatini oladi. tekshir yordamchisi kerak emas — uning ishini test va assert qiladi.

Xulqni qotiruvchi testlar. Eski skriptlardagi tekshiruvlarga qo'shimcha, ilovaning hozirgi xulqini yozib qo'yadigan testlar qo'shiladi. Ular "to'g'ri xulq qanday bo'lishi kerak" degan savolga emas, "hozir aynan nima qilyapti" degan savolga javob beradi. Bunday testlar characterization tests (xulqni qotiruvchi testlar) deyiladi — ularni Legacy kod va texnik qarz darsida yana uchratamiz. Ular keyingi darsdagi refaktordan oldin "xavfsizlik to'ri" bo'ladi. Masalan:

  • eksportMatni ning aniq matni: 2 bo'sh joy, maydonlar tartibi, begona maydon chiqmaydi;
  • paketniOqi ning har xatosi — turi, kod va matni (JSON, TUZILMA, VERSIYA, VAZIFA, cause);
  • saqla xotira to'lganda false qaytaradi, yukla xotira yopiq bo'lsa null;
  • filtrniYoz URL o'zgarmasa pushState chaqirmaydi; holat.js ning xato matnlari;
  • api.js — har metodning HTTP metodi, manzili, tanasi va sarlavhalari, status → kod jadvali;
  • royxat.almashtir(99) (noma'lum id) — TypeError.

Xato matnini tekshiradigan yordamchi (paket.test.js dan) — throws ning ikkinchi argumenti funksiya ham bo'lishi mumkin:

js
// Xato turi, kodi va matni — uchalasi ham foydalanuvchiga ko'rinadi
function xatoBilan(matn, kod, xabar) {
  assert.throws(() => paketniOqi(matn), (xato) => {
    assert.ok(xato instanceof VazifaXatosi);
    assert.equal(xato.name, "VazifaXatosi");
    assert.equal(xato.kod, kod);
    assert.equal(xato.message, xabar);
    return true;
  });
}

api.js testlari internetsiz ishlaydi: global fetch o'rniga soxta funksiya qo'yiladi (api.test.js):

js
// fetch chaqiruvlarini yozib boradi va berilgan javobni qaytaradi
function soxtaFetch(javob = () => new Response("null")) {
  return mock.method(globalThis, "fetch", async (url, sozlama) =>
    javob(url, sozlama));
}

afterEach(() => mock.restoreAll());

mock.method(obyekt, "nomi", yangiFunksiya) obyektning metodini vaqtincha almashtiradi va har chaqiruvni fetch.mock.calls ga yozadi. Test shu ro'yxatdan "qaysi URL'ga, qaysi metod bilan so'rov ketdi?" deb so'raydi. afterEach da mock.restoreAll() — asl fetch qaytariladi, keyingi testga soxtasi o'tib ketmaydi.

Natija. npm test — haqiqiy chiqishning boshi va oxiri:

text
▶ apiKlient so'rovlari
  ✔ royxat — GET, tanasiz (45.2753ms)
  ✔ qosh, yangila, ochir, almashtir — metod, manzil, tana (2.0672ms)
  ✔ muvaffaqiyatsiz: faqat yozuvlarga ?muvaffaqiyatsiz=1 (1.6976ms)
  ✔ bo'sh javob — null (0.5178ms)
✔ apiKlient so'rovlari (51.3082ms)
...
ℹ tests 57
ℹ suites 10
ℹ pass 57
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 239.7291

57 test, 10 guruh, chorak soniya. royxat.test.js da 18 ta test, shundan 5 tasi Unicode regex darsidagi teg tekshiruvlari — kengaytirildi: bo'sh joysiz "Sabzi #bozor,#uy.", U+02BB kod nuqtasi bilan oʻ, '#o'quv', ##bozor, #ДОМ. npm run test:tarmoq (haqiqiy API) — 2/2. assets/js ga tegilmadi.

Testlar haqiqatan himoya qiladimi? Qizilni ko'ramiz. royxat.js ga vaqtincha eski (v3) teglariniOl ni — split(" ") bilan ishlaydiganini — qaytarib, node --test tekshiruv/royxat.test.js ni ishga tushirdik. Yiqilgan to'rtta test (har biridan faqat nomi va actual qiymati qoldirilgan):

text
✖ teg: vergul va nuqta tegga yopishmaydi      actual: [ '#bozor,', '#uy' ]
✖ teg: o'zbekcha apostrof — ', ʻ (U+02BB), ’  actual: []   expected: [ "#o'quv" ]   ('#o'quv' dan)
✖ teg: emoji tegga kirmaydi                   actual: [ '#bozor🛒', '#🛒' ]
✖ teg emas: # yolg'iz, ##, so'z ichidagi #    actual: [ '##', '##bozor' ]

Hisobot: tests 18 · pass 14 · fail 4. Testlar eski xatoni darhol ushladi — endi u jimgina qaytib kela olmaydi. Yangi kodni qaytargach — hammasi ✔.

Yechim (buyruqlar)
bash
git switch -c chore/node-test
# ... testlarni yozing, eski skriptlarni o'chiring ...
npm test
git add package.json README.md tekshiruv
git commit -m "test: tekshiruv skriptlari node:test testlariga \
aylantirildi, xulqni qotiruvchi testlar qo'shildi"
git push -u origin chore/node-test
gh pr create --fill
gh pr merge --merge

git add tekshiruv o'chirilgan fayllarni ham commit'ga qo'shadi. Commit turi — test: (testlar qo'shildi), branch — chore/ (ilova xulqiga tegmaydigan ish). Diff: 14 fayl, +666 −174 — asosan yangi testlar.

Bundan keyin har refaktor qadamida tekshiruv bitta buyruq: npm test. Keyingi darsda aynan shu testlar himoyasida kod hidlarini tuzatamiz.

9. Real ishda

  • Har jiddiy loyihada testlar bor. Pull request ochilganda testlar avtomatik ishga tushadi (CI — uzluksiz integratsiya); bittasi yiqilsa, PR birlashtirilmaydi.
  • Test kutubxonalari. Frontend'da ko'pincha Vitest yoki Jest, Node backend'da node:test ham, ular ham. Tuzilishi bir xil: describe, test/it, tekshiruv. node:test ni bilgan odam ularga bir kunda o'tadi.
  • Test turlari. Bugungi testlar — unit testlar: bitta funksiya yoki modul. Undan kattaroqlari — integratsiya testlari (bir nechta modul birga, api.tarmoq.js kabi haqiqiy server bilan) va E2E (end-to-end) testlar: haqiqiy brauzerda foydalanuvchi kabi bosish. Ularni 22-qismda o'rganamiz.
  • Intervyu. "Unit test nima?", "Qanday holatlarni test qilasiz?", "TDD nima?", "Mock nima uchun kerak?" — junior intervyusining odatiy savollari. Ko'pincha kichik funksiya berib, "unga test yozing" deyishadi.

Xulosa

  • Avtomatik test — kodni tekshiradigan kod: bir marta yoziladi, bitta buyruq bilan har safar hamma holatni tekshiradi.
  • node:assert/strict: equal (son, satr, null), deepEqual (massiv, obyekt), throws (arrow bilan), ok, match; asinxronda — await assert.rejects.
  • node:test: test(nomi, fn), guruh — describe (it = test), tayyorgarlik — beforeEach; har test mustaqil.
  • node --test *.test.js fayllarni o'zi topadi; package.json da "test": "node --test" → npm test.
  • Chegaraviy holatlar: chegaradan bitta kam, aynan o'zi, bitta ko'p; bo'sh va noto'g'ri kirish.
  • Qizil → yashil → toza: xatoni avval testga aylantiring va yiqilishini ko'ring.

Keyingi dars: Code smell va refactoring — koddagi "hid"larni tanib, testlar himoyasida kichik va xavfsiz qadamlar bilan tuzatamiz.

Manbalar

  • Node.js hujjatlari: "Test runner" (node:test) — nodejs.org/api/test.html (v24)
  • Node.js hujjatlari: "Assert" (node:assert/strict) — nodejs.org/api/assert.html
  • Kent Beck, "Test-Driven Development: By Example", 2002
  • Michael Feathers, "Working Effectively with Legacy Code", 2004 — characterization tests
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Birinchi avtomatik test: node:test va assert — kodni o'zi tekshiradigan kod — IlmHamroh