Kodax

الاختبارات الآلية بـ node:test — Node.js و Express

الاختبارات الآلية بـ node:test

كل مرة تعدّل كوداً، تجرّبه يدوياً؟ مع 30 مساراً و10 حالات لكل منها، يصبح ذلك مستحيلاً — فتصل الأخطاء للمستخدمين. الاختبارات الآلية تجرّب كل شيء في ثوانٍ بعد كل تعديل.

مشغّل مدمج — بلا مكتبات

منذ Node 20، node:test مستقر ومدمج:

const { test, describe, it } = require("node:test");
const assert = require("node:assert/strict");

function slugify(text) {
  return text.trim().toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-|-$/g, "");
}

describe("slugify", () => {
  it("يحوّل المسافات إلى شرطات", () => {
    assert.equal(slugify("Hello World"), "hello-world");
  });
  it("يحافظ على الحروف العربية", () => {
    assert.equal(slugify("  مرحبا يا عالم "), "مرحبا-يا-عالم");
  });
  it("يزيل الرموز من الأطراف", () => {
    assert.equal(slugify("!!Node.js!!"), "node-js");
  });
});
node --test                 # يشغّل كل ملفات *.test.js و test/*.js
node --test --watch         # يعيد التشغيل عند الحفظ
node --test --experimental-test-coverage

أضف إلى package.json: "test": "node --test" ثم npm test.

assert: أهم الدوال

| الدالة | للتحقق من |

|---|---|

| assert.equal(a, b) | تساوٍ صارم (مع /strict) |

| assert.deepEqual(obj1, obj2) | كائنات ومصفوفات بالمحتوى |

| assert.ok(value) | قيمة صحيحة (truthy) |

| assert.throws(fn, /رسالة/) | دالة ترمي خطأ |

| await assert.rejects(promise, ErrorClass) | Promise يُرفض |

| assert.match(text, /نمط/) | نص يطابق نمطاً |

غير المتزامن

اجعل الاختبار async وانتظر — المشغّل ينتظر Promise:

const { test } = require("node:test");
const assert = require("node:assert/strict");

async function fetchUser(id) {
  if (id <= 0) throw new RangeError("معرّف غير صالح");
  return { id, name: "سارة" };
}

test("يعيد المستخدم", async () => {
  assert.deepEqual(await fetchUser(1), { id: 1, name: "سارة" });
});
test("يرفض المعرّف السالب", async () => {
  await assert.rejects(fetchUser(-1), RangeError);
});

الدوال الوهمية (Mocks) والاعتماديات

اختبار يرسل بريداً حقيقياً أو يكتب في قاعدة الإنتاج ليس اختباراً بل كارثة. صمم الكود ليستقبل اعتمادياته (حقن الاعتماديات)، فتمرر نسخاً وهمية في الاختبار:

const { test, mock } = require("node:test");
const assert = require("node:assert/strict");

function makeSignup({ usersRepo, mailer }) {
  return async (email) => {
    if (await usersRepo.exists(email)) throw new Error("مستخدم موجود");
    await usersRepo.create(email);
    await mailer.send(email, "مرحباً بك");
  };
}

test("التسجيل يرسل بريد ترحيب مرة واحدة", async () => {
  const usersRepo = { exists: async () => false, create: mock.fn(async () => {}) };
  const mailer = { send: mock.fn(async () => {}) };
  await makeSignup({ usersRepo, mailer })("sara@example.com");
  assert.equal(mailer.send.mock.callCount(), 1);
  assert.deepEqual(mailer.send.mock.calls[0].arguments, ["sara@example.com", "مرحباً بك"]);
});

ما الذي يُختبر؟

| النوع | ماذا | السرعة والعدد |

|---|---|---|

| وحدة (unit) | دالة أو خدمة معزولة عن الشبكة وقاعدة البيانات | سريعة جداً، كثيرة |

| تكامل (integration) | API مع قاعدة بيانات اختبار حقيقية | أبطأ، أقل |

| شامل (E2E) | التطبيق كاملاً كما يستخدمه المستخدم | الأبطأ، قليلة للمسارات الحرجة |

اختبر السلوك لا التفاصيل الداخلية: "التسجيل ببريد موجود يرفض" لا "الدالة استدعت المتغير x". واختبر حالات الحافة والفشل — هناك تختبئ الأخطاء: مدخلات فارغة، قيم سالبة، صلاحيات، تكرار.

التغطية: مؤشر لا هدف

تغطية 100% لا تعني خلو الكود من الأخطاء؛ تعني أن كل سطر نُفِّذ مرة. استخدمها لاكتشاف الأجزاء غير المختبرة إطلاقاً، لا كرقم للتباهي.

في كل مشروع

- شغّل الاختبارات في CI (GitHub Actions) مع كل Pull Request.

- عند إصلاح خطأ: اكتب أولاً اختباراً يفشل بسببه، ثم أصلحه — فلا يعود.

تمرين

اكتب اختبارات لدالة parsePagination(query) من درس الترقيم: القيم الافتراضية، القص عند limit كبير، page سالب، نصوص غير رقمية. ثم دالة transfer بمستودع وهمي: تحويل ناجح، رصيد غير كافٍ، مبلغ سالب.

// cart.test.js — شغّله بـ: node --test (أو node cart.test.js)
const { describe, it, beforeEach, mock } = require("node:test");
const assert = require("node:assert/strict");

// ===== الكود المُختبَر: سلة مشتريات تستقبل اعتمادياتها =====
function createCart({ pricing, clock = () => new Date() }) {
  const items = new Map();
  return {
    add(sku, qty = 1) {
      if (!Number.isInteger(qty) || qty < 1) throw new RangeError("كمية غير صالحة");
      items.set(sku, (items.get(sku) ?? 0) + qty);
    },
    remove(sku) {
      items.delete(sku);
    },
    async total(coupon) {
      let sum = 0;
      for (const [sku, qty] of items) sum += (await pricing.priceOf(sku)) * qty;
      if (coupon === "RAMADAN" && clock().getMonth() === 2) sum *= 0.9;   // خصم في مارس فقط
      return Math.round(sum * 100) / 100;
    },
    count: () => [...items.values()].reduce((a, b) => a + b, 0),
  };
}

// ===== الاختبارات =====
describe("سلة المشتريات", () => {
  let pricing;
  beforeEach(() => {
    pricing = { priceOf: mock.fn(async (sku) => ({ PEN: 1.5, BAG: 25 })[sku] ?? 0) };
  });

  it("تجمع كميات نفس المنتج", () => {
    const cart = createCart({ pricing });
    cart.add("PEN", 2);
    cart.add("PEN", 3);
    assert.equal(cart.count(), 5);
  });

  it("ترفض الكميات غير الصالحة", () => {
    const cart = createCart({ pricing });
    assert.throws(() => cart.add("PEN", 0), RangeError);
    assert.throws(() => cart.add("PEN", 1.5), /كمية غير صالحة/);
  });

  it("تحسب المجموع من خدمة الأسعار", async () => {
    const cart = createCart({ pricing });
    cart.add("PEN", 4);
    cart.add("BAG");
    assert.equal(await cart.total(), 31);
    assert.equal(pricing.priceOf.mock.callCount(), 2);
  });

  it("تطبق الكوبون في الشهر الصحيح فقط (ساعة وهمية)", async () => {
    const march = createCart({ pricing, clock: () => new Date("2026-03-10") });
    const july = createCart({ pricing, clock: () => new Date("2026-07-10") });
    march.add("BAG", 2);
    july.add("BAG", 2);
    assert.equal(await march.total("RAMADAN"), 45);
    assert.equal(await july.total("RAMADAN"), 50);
  });

  it("الحذف يزيل المنتج كلياً", async () => {
    const cart = createCart({ pricing });
    cart.add("PEN", 2);
    cart.remove("PEN");
    assert.equal(await cart.total(), 0);
  });
});
اختبر فهمك