JavaScript بعمق
اختبار JavaScript: الثقة عبر الاختبارات الآلية
دليل عملي لاختبار JavaScript — لماذا تهمّ الاختبارات، والوحدة مقابل التكامل مقابل الطرف-إلى-الطرف، وبنية ترتيب-تنفيذ-تأكيد، وكتابة الاختبارات بـ Vitest، والتأكيدات والمطابقات، والمحاكاة، واختبار الشيفرة غير المتزامنة، وماذا تختبر (وماذا لا)، والتطوير المقاد بالاختبار — مع تمارين عملية وحلولها.
الاختبارات هي كيف تتوقّف عن الخوف من شيفرتك. بدونها، كل تغيير مقامرة — هل يكسر هذا شيئًا في ملفّ على بعد ثلاثة ملفّات؟ معها، تغيّر الشيفرة بثقة واختبار أحمر يخبرك لحظة تراجُع شيء. الاختبار ليس تخصّصًا منفصلًا تركّبه في النهاية؛ بل عادة تجعلك تكتب شيفرة أحسن بنيةً من البداية. هذا المقال يغطّي النواة العملية: كيف تكتب اختبارات جيّدة، وماذا تختبر، والأدوات لفعل ذلك. (الدوال النقية من JavaScript الوظيفية أسهل ما يُختبَر، وهذا جزء من سبب جدوى ذلك الأسلوب.)
الاختبار مجرّد شيفرة تشغّل شيفرتك وتؤكّد أن النتيجة كما تتوقّع. قيمته الحقيقية ليست التقاط العلل اليوم — بل الثقة بتغيير الأشياء غدًا، عالمًا أن المجموعة ستلتقط أي شيء تكسره.
هرم الاختبار
الاختبارات تأتي طبقات، والمجموعة الصحّية فيها أكثر من الرخيصة السريعة:
- اختبارات الوحدة — تختبر دالّة أو وحدة واحدة منعزلةً. سريعة، كثيرة، تحدّد الإخفاقات بدقّة. قاعدة الهرم.
- اختبارات التكامل — تختبر عدّة قطع تعمل معًا (مكوّن مع طبقة بياناته). أقلّ، أبطأ، تلتقط علل التوصيل.
- اختبارات الطرف-إلى-الطرف (E2E) — تقود التطبيق الحقيقي في متصفح كما يفعل مستخدم (بأدوات مثل Playwright). قليلة، بطيئة، لكنها تتحقّق أن كل شيء يعمل.
اهدف إلى اختبارات وحدة كثيرة، بعض التكامل، قليل من E2E. عكس ذلك — كثير من اختبارات E2E البطيئة، قليل من الوحدات — يعطي مجموعة بطيئة ومتقلّبة.
تشريح اختبار
كل اختبار يتبع الشكل الثلاثي نفسه، ترتيب–تنفيذ–تأكيد (Arrange–Act–Assert):
import { describe, it, expect } from "vitest";
function add(a, b) { return a + b; }
describe("add", () => {
it("sums two numbers", () => {
const a = 2, b = 3; // ترتيب — هيّئ المُدخَلات
const result = add(a, b); // تنفيذ — شغّل الشيء
expect(result).toBe(5); // تأكيد — افحص النتيجة
});
});
describeيجمّع الاختبارات المترابطة.it(أوtest) حالة اختبار واحدة — سمِّها كجملة تصف السلوك.expect(...).toBe(...)هو التأكيد — يُفشِل الاختبار إن لم تكن القيمة كما تتوقّع.
أسماء الاختبارات الجيّدة تُقرأ كمواصفة: "add sums two numbers"، "throws on negative input".
التأكيدات والمطابقات
expect تقدّم مطابقات لأنواع فحوص مختلفة:
expect(2 + 2).toBe(4); // مساواة صارمة (===) للأوّليات
expect({ a: 1 }).toEqual({ a: 1 }); // مساواة عميقة للكائنات/المصفوفات
expect([1, 2, 3]).toContain(2);
expect("hello").toMatch(/ell/); // تعبير نمطيّ
expect(value).toBeNull();
expect(value).toBeTruthy();
expect(() => parse("")).toThrow(); // يتوقّع أن ترمي الدالّة
expect(arr).toHaveLength(3);
التمييز الحاسم: toBe يستخدم === (صحيح للأرقام والسلاسل والقيم المنطقية والمرجع نفسه)، بينما toEqual يقارن البنية (صحيح للكائنات والمصفوفات، التي ليست === حتى لو تطابق محتواها).
المحاكاة (Mocking)
حين تعتمد الشيفرة على شيء بطيء أو خارجيّ أو غير متوقّع (API، مؤقّت، التاريخ الحالي)، تحاكيه — تستبدله ببديل مضبوط كي يبقى الاختبار سريعًا وحتميًّا:
import { vi, expect, it } from "vitest";
it("calls the callback once", () => {
const cb = vi.fn(); // دالّة محاكاة تسجّل الاستدعاءات
doWork(cb);
expect(cb).toHaveBeenCalledOnce();
expect(cb).toHaveBeenCalledWith("done");
});
// حاكِ وحدة كاملة (مثل الشبكة)
vi.mock("./api", () => ({
fetchUser: vi.fn(() => Promise.resolve({ name: "Ada" })),
}));
vi.fn() يصنع جاسوسًا تستطيع تأكيد أنه استُدعي (وكيف). حاكِ الحدود — الشبكة، نظام الملفّات، العشوائية، الوقت — كي يمرّن اختبارك منطقك، لا خادم شخص آخر.
اختبار الشيفرة غير المتزامنة
الاختبارات غير المتزامنة تنتظر الشيء بـ await وتؤكّد على النتيجة — دالّة الاختبار async:
it("loads a user", async () => {
const user = await loadUser(1);
expect(user.name).toBe("Ada");
});
it("rejects on a bad id", async () => {
await expect(loadUser(-1)).rejects.toThrow();
});
.resolves/.rejects تتيحان التأكيد على نتيجة وعد مباشرةً. المفتاح أن تنتظر (أو تُرجِع) التأكيد كي ينتظره الاختبار — نسيان ذلك يجعل الاختبار ينجح قبل انتهاء العمل غير المتزامن.
ماذا تختبر (وماذا لا)
الاختبار الجيّد عن ماذا تغطّي، لا مطاردة 100%:
- اختبر السلوك، لا التنفيذ — أكّد ماذا تُرجِع دالّة، لا كيف تحسبه، كي لا تكسر إعادات الهيكلة الاختبارات.
- رتّب الأولويات: المسارات الحرجة، والحالات الحدّية (مُدخَل فارغ، صفر، سالب، null)، وأي شيء انكسر سابقًا.
- لا تختبر اللغة أو المكتبات (أن
Array.mapيعمل)، أو الـ getters التافهة، أو الدواخل الخاصّة. - الدوال النقية أسهل — نفس المُدخَل، نفس المُخرَج، بلا إعداد — وهذا سبب لهيكلة المنطق هكذا.
اختبار ينكسر كلما أعدت الهيكلة دون تغيير السلوك يختبر الشيء الخطأ.
التطوير المقاد بالاختبار، باختصار
TDD يقلب الترتيب: اكتب اختبارًا فاشلًا أولًا، ثم الشيفرة لتنجيحه، ثم أعد الهيكلة — حلقة "أحمر، أخضر، أعد الهيكلة":
// 1. أحمر — اكتب الاختبار لسلوك لا يوجد بعد
it("formats a price", () => {
expect(formatPrice(5)).toBe("$5.00");
});
// 2. أخضر — اكتب أدنى formatPrice لتنجيحه
// 3. أعد الهيكلة — نظّف، والاختبار يحرسك
لست مضطرًّا لـ TDD دائمًا، لكن كتابة الاختبار أولًا توضّح ماذا ينبغي أن تفعل الدالّة وتضمن أن الاختبار يفشل فعلًا حين يغيب السلوك.
الأخطاء الشائعة
- اختبار تفاصيل التنفيذ بدل السلوك، فتكسر إعادات الهيكلة غير الضارّة الاختبارات.
- نسيان
await/إرجاع تأكيد غير متزامن، فينجح الاختبار قبل انتهاء العمل. - استخدام
toBeعلى الكائنات/المصفوفات (إنه===، فيفشل) — استخدمtoEqual. - عدم محاكاة الحدود الخارجية، فتصير الاختبارات بطيئة أو متقلّبة أو معتمِدة على الشبكة.
- مطاردة تغطية 100% باختبار شيفرة تافهة، مع تفويت الحالات الحدّية الحرجة.
- اختبارات تعتمد إحداها على الأخرى أو على الترتيب — كلٌّ ينبغي أن يكون مستقلًّا وقابلًا للتكرار.
- كتابة اختبار عملاق واحد يؤكّد عشرة أشياء — الاختبارات الصغيرة المركّزة تحدّد الإخفاقات بدقّة.
تمارين
جرّب كلًّا منها قبل فتح الحل.
تمرين 1 — أول اختبار وحدة
اكتب اختبارًا لـ function isEven(n) { return n % 2 === 0; }.
إظهار الحل
import { describe, it, expect } from "vitest";
describe("isEven", () => {
it("is true for even numbers", () => {
expect(isEven(4)).toBe(true);
});
it("is false for odd numbers", () => {
expect(isEven(3)).toBe(false);
});
});
حالتان تغطّيان السلوك: مسار صحيح ومسار خاطئ. toBe صحيح هنا لأن النتيجة قيمة منطقية أوّلية.
تمرين 2 — toBe مقابل toEqual
لماذا يفشل expect({ a: 1 }).toBe({ a: 1 })، وماذا ينبغي أن تستخدم؟
إظهار الحل
toBe يستخدم ===، الذي للكائنات يفحص هوية المرجع — كائنان حرفيّان مختلفان ليسا === أبدًا، حتى بمحتوى متطابق، فيفشل. استخدم toEqual، الذي يقارن البنية:
expect({ a: 1 }).toEqual({ a: 1 }); // ينجح
تمرين 3 — اختبر دالّة غير متزامنة
اكتب اختبارًا أن loadUser(1) يُحَلّ إلى كائن name فيه "Ada".
إظهار الحل
it("loads Ada", async () => {
const user = await loadUser(1);
expect(user.name).toBe("Ada");
});
دالّة الاختبار async تنتظر الوعد كي يعمل التأكيد على القيمة المُحَلّة — بلا await، سينتهي الاختبار قبل وصول البيانات.
تمرين 4 — اختبر أنها ترمي
withdraw(amount) ينبغي أن ترمي على مبلغ سالب. اختبره.
إظهار الحل
it("rejects negative amounts", () => {
expect(() => withdraw(-5)).toThrow();
});
تمرّر دالّة إلى expect (لا نتيجة الاستدعاء)، كي يستطيع المطابِق استدعاءها والتقاط الرمي — استدعاء withdraw(-5) مباشرةً سيرمي قبل أن يعمل expect أصلًا.
النموذج الذهني الذي تحتفظ به
الاختبار يشغّل شيفرتك ويؤكّد النتيجة — مكسبه الثقة بتغيير الشيفرة غدًا. فضّل الهرم: كثير من اختبارات الوحدة السريعة، بعض التكامل، قليل من E2E. هيكِل كلًّا كـ ترتيب–تنفيذ–تأكيد، وسمِّه كسلوك، واختر المطابِق الصحيح — toBe للأوّليات/المراجع، toEqual لبنية الكائنات. حاكِ الحدود (الشبكة، الوقت، العشوائية) كي تكون الاختبارات سريعة وحتمية، وانتظر تأكيداتك غير المتزامنة بـ await، واختبر السلوك، لا التنفيذ، كي تبقى إعادات الهيكلة خضراء. غطِّ المسارات الحرجة والحالات الحدّية بدل مطاردة 100%. اكتب الاختبارات كعادة — أحيانًا أولًا، بأسلوب TDD — فتصير قاعدتك شيئًا تستطيع تغييره بلا خوف بدل التحرّك حوله على أطراف أصابعك.