الوحدة 1 · لماذا نختبر، وماذا نختبر
تشريح الاختبار وقراءة رسائل الفشل
18 دقراءة 4 دبيئة تجريب الكود
الدرس في لمحة
تبني كل اختبار بنمط Arrange وAct وAssert، وتختار بين toBe وtoEqual، وتقرأ رسالة فشل Vitest لتصل إلى السطر المعطوب في ثوانٍ.
- 1Arrange وAct وAssert
- 2الأسماء هي السطر الأول في تقرير الخطأ
- 3toBe أم toEqual؟
- 4قراءة رسالة الفشل
- 5اختبار أن شيئًا ما يرمي خطأً
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
يُقرأ الاختبار أكثر بكثير مما يُكتب. يُقرأ حين يفشل في لحظة سيئة، على يد شخص لم يكتبه، يريد إجابة واحدة: ما الذي انكسر؟ هذا الدرس عن كتابة اختبارات تجيب عن هذا السؤال في ثوانٍ، وعن قراءة الإجابة التي تعطيك إياها Vitest.
Arrange وAct وAssert
لكل اختبار جيد تقريبًا الأجزاء الثلاثة نفسها:
import { test, expect } from 'vitest';
import { withTip } from './tip.js';
test('rounds a half-cent tip up to the next cent', () => {
// Arrange: the inputs that matter for this behaviour
const subtotalCents = 1980;
const percent = 12.5;
// Act: one call to the code under test
const result = withTip(subtotalCents, percent);
// Assert: the claims about the outcome
expect(result).toEqual({ tipCents: 248, totalCents: 2228 });
});Arrange (التهيئة) يبني العالم الذي يحتاجه الاختبار. Act (الفعل) يفعل شيئًا واحدًا فقط: السلوك الذي تختبره. Assert (التأكيد) يذكر ما يجب أن يكون صحيحًا الآن. لن تحتاج إلى التعليقات بعد أن يصبح هذا الشكل عادة؛ الأسطر الفارغة تكفي.
يحميك هذا الشكل من أشهر رائحة سيئة في الاختبارات: اختبار يفعل، ثم يؤكّد، ثم يفعل مرة أخرى، ثم يؤكّد مرة أخرى. حين يفشل، لا تستطيع أن تعرف أيّ خطوة انكسرت. قسّمه إلى اختبارين، لكل منهما اسم يقول ما الذي يثبته.
كم assertion في الاختبار الواحد؟
ستسمع قاعدة “assertion واحد لكل اختبار”، وهي قاعدة صارمة أكثر من اللازم. القاعدة الحقيقية هي سلوك واحد لكل اختبار. فحص tipCents وtotalCents معًا بعد استدعاء واحد هو سلوك واحد له حقيقتان يمكن ملاحظتهما، فلا بأس بتأكيد الاثنتين، وtoEqual على الكائن كله يفعل ذلك في سطر واحد. ما تريد تجنّبه هو اختبار يفحص التقريب، ثم يستدعي الدالة مجددًا ليفحص حالة الخطأ، ثم مرة ثالثة ليفحص الصفر. حين يفشل الادّعاء الأول، توقف Vitest ذلك الاختبار، فلا تعرف أبدًا إن كان السلوكان الآخران ما زالا يعملان. ثلاثة اختبارات صغيرة تعطيك ثلاث إجابات منفصلة.
الأسماء هي السطر الأول في تقرير الخطأ
حين يفشل اختبار، يطبع التقرير مساره الكامل: withTip > rounds a half-cent tip up to the next cent. اكتب ذلك الاسم جملةً عن السلوك. “withTip works” لا تخبرك بشيء؛ أما “rejects a negative tip percent” فتخبرك بما تراجع قبل أن تفتح أي ملف. اجمع بـ describe حسب الوحدة، وسمِّ بـ test حسب القاعدة.
toBe أم toEqual؟
يقوم اثنان من الـ matchers (أدوات المطابقة) بمعظم العمل، ويقارن كلٌّ منهما بطريقة مختلفة:
toBeيستخدمObject.is: القيمة البدائية نفسها، أو مرجع الكائن نفسه. استخدمه مع الأرقام والنصوص والقيم المنطقية.toEqualيقارن البنية بشكل متكرّر: المفاتيح نفسها، والقيم نفسها. استخدمه مع الكائنات والمصفوفات. (ويفحصtoStrictEqualأيضًا تطابق الخصائص التي قيمتهاundefinedوأنواع الأصناف.)
expect(withTip(2000, 15)).toBe({ tipCents: 300, totalCents: 2300 }) يفشل رغم تطابق القيم، لأن الدالة تعيد كائنًا جديدًا. هذا الفخ يقع فيه الجميع مرة واحدة على الأقل.
قراءة رسالة الفشل
إليك نوعًا حقيقيًا من الأخطاء. النسخة الأولى من toCents، التي تحوّل مبلغًا بالدولار إلى سنتات، استخدمت Math.floor:
const toCents = (dollars) => Math.floor(dollars * 100);
console.log(0.29 * 100); // what the computer actually computes
console.log(toCents(0.29)); // one cent short
console.log(Math.round(0.29 * 100));يفشل الاختبار expect(toCents(0.29)).toBe(29)، وتعرض Vitest شيئًا كهذا:
FAIL src/money.test.js > toCents > converts 0.29 dollars to 29 cents
AssertionError: expected 28 to be 29 // Object.is equality
- Expected
+ Received
- 29
+ 28
❯ src/money.test.js:7:28اقرأها من الأعلى إلى الأسفل، على ثلاث مراحل:
- المسار يخبرك أيّ سلوك انكسر: تحويل 0.29 دولار.
- المتوقّع مقابل المستلم يخبرك كيف انكسر: ينقص سنتًا واحدًا، إذن شيء ما اقتطع القيمة بدل أن يقرّبها.
- الموقع (
money.test.js:7:28) يشير إلى سطر الـ assertion في الاختبار. والخطأ موجود في الكود الذي يستدعيه ذلك السطر.
مع الكائنات، يعلّم الفرق (diff) الأسطر المختلفة فقط، فيبرز totalCents الخاطئ داخل كائن من عشرة مفاتيح فورًا.
اختبار أن شيئًا ما يرمي خطأً
أحيانًا يكون السلوك المطلوب هو “رفض المدخلات السيئة”. لفّ الاستدعاء داخل دالة كي يستطيع expect أن يستدعيها ويلتقط الخطأ:
test('rejects a negative tip percent', () => {
expect(() => withTip(1000, -5)).toThrow(RangeError);
});بدون الـ arrow function، ترمي withTip الخطأ أثناء حساب المعاملات، قبل أن يعمل expect، فينهار الاختبار بدل أن ينجح. ويقبل toThrow أيضًا نصًّا أو تعبيرًا نمطيًا (regex) لمطابقة رسالة الخطأ.
في التمرين تكتب اختبارات بنمط AAA لـ withTip، بما فيها الحدّ الذي يسهل نسيانه: بقشيش قيمته صفر. وينتقل القسم التالي من “كيف يبدو الاختبار” إلى “أيّ الاختبارات تستحق أن تُكتب”.
أهم النقاط
- يعطي نمط Arrange وAct وAssert كل اختبار الشكل الثلاثي نفسه، فيجد القارئ التهيئة والفعل والادّعاء دون بحث.
- يفحص الاختبار الواحد سلوكًا واحدًا، واسمه جملة تخبرك أيّ سلوك انكسر حين يفشل.
- يقارن
toBeباستخدامObject.is(القيمة نفسها أو المرجع نفسه)؛ أماtoEqualفيقارن البنية، وهذا ما تريده مع الكائنات والمصفوفات. - يعطيك تقرير الفشل مسار الاختبار، والقيمتين المتوقّعة والمستلمة، والملف ورقم السطر؛ اقرأ الثلاثة قبل أن تلمس الكود.