الوحدة 5 · استراتيجية الاختبار
التطوير الموجّه بالاختبارات كأداة
20 دقراءة 4 دبيئة تجريب الكود
الدرس في لمحة
تستخدم دورة red-green-refactor لبناء منطق محدّد المواصفات ولإصلاح الأخطاء، وتميّز الحالات التي تبطّئك فيها كتابة الاختبارات أولًا.
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
للتطوير الموجّه بالاختبارات (Test-driven development أو TDD) مشكلة في سمعته. بعض الناس يعاملونه كدين؛ وآخرون يرفضونه بعد تمرين واحد مؤلم. أنا أعامله كأداة، ومثل أي أداة فهو ممتاز لبعض المهام وخاطئ لأخرى. أما بالنسبة إلى منطق المال الأساسي في Splitwise-lite، فهو أفضل أداة أعرفها.
الدورة
الـ TDD حلقة قصيرة، تستغرق عادةً دقيقة أو اثنتين في كل جولة:
- Red (الأحمر): اكتب اختبارًا صغيرًا واحدًا لسلوك غير موجود بعد. شغّله. وشاهده يفشل، ويُفضَّل أن يكون بالفشل الذي توقّعته.
- Green (الأخضر): اكتب أبسط كود يجعله ينجح. بسيط، لا ذكي؛ والتثبيت الصريح للقيم (hard-coding) مسموح به إن كان صادقًا.
- Refactor (إعادة الهيكلة): نظّف الكود (والاختبارات) وكل شيء يبقى أخضر.
لماذا الإصرار على رؤية الأحمر؟ لأن الاختبار الذي لم تره يفشل قط قد لا يكون قادرًا على الفشل. قد يستدعي الدالة الخاطئة، أو يؤكّد متغيّرًا قيمته truthy دائمًا، أو لا يفحص شيئًا في الحقيقة (await مفقودة، أو .skip منسيّة في مكانها). الأحمر أولًا دليل رخيص على أن الاختبار موصول بشكل صحيح.
بناء settleUp بكتابة الاختبار أولًا
يحتاج Splitwise-lite إلى settleUp(balances): بمعرفة من له ومن عليه (بالسنتات)، تعيد قائمة قصيرة من التحويلات تعيد الجميع إلى الصفر. إليك كيف تسير الجولات الأولى، مع Vitest في وضع المراقبة كي يعيد كل حفظ تشغيل الاختبارات.
الجولة 1، أبسط حالة. لا أحد مدين بشيء:
test('needs no transfers when everyone is settled', () => {
expect(settleUp({ Ana: 0, Ben: 0 })).toEqual([]);
});الأحمر: settleUp is not defined. الأخضر: export function settleUp() { return []; }. نعم، هذا هو التطبيق كله في الوقت الحالي. إنه صادق بشأن ما تطلبه الاختبارات.
الجولة 2، دين واحد. يفرض الاختبار التالي سلوكًا حقيقيًا:
test('the debtor pays the creditor', () => {
expect(settleUp({ Ana: 500, Ben: -500 })).toEqual([
{ from: 'Ben', to: 'Ana', amountCents: 500 },
]);
});قد يقرن الأخضر الرصيد السالب الوحيد بالرصيد الموجب الوحيد. ما زالت خطوة صغيرة.
الجولة 3، عدة أشخاص. الآن ينكسر الاقتران المثبّت يدويًا، وتستحق الخوارزمية العامة مكانها: رتّب المدينين والدائنين حسب المبلغ، وانقل مرة بعد مرة الأصغر من المبلغين الحاليين، وتقدّم في الجهة التي تصل إلى الصفر. وفي الـ assertions، استخدم الخصائص حيث تكون عدة إجابات صالحة: تطبيق التحويلات يعيد كل رصيد إلى الصفر، وكل مبلغ موجب، وعدد التحويلات لا يتجاوز people - 1.
الجولة 4، مدخل غير صالح. الأرصدة التي لا يساوي مجموعها صفرًا تعني خطأً في مرحلة سابقة؛ ويجب أن ترفضها settleUp بدل أن تختلق مالًا: expect(() => settleUp({ Ana: 500, Ben: -400 })).toThrow().
ثم أعد الهيكلة: سمِّ دالة المقارنة في الترتيب، واستخرج دالة مساعدة، واجعل الكود سهل القراءة. تتيح لك الاختبارات تغيير شكل الكود دون خوف، وهذا هو المكسب الحقيقي.
إصلاح الأخطاء بكتابة الاختبار أولًا
أثمن استخدام يومي للـ TDD لا علاقة له بالميزات الجديدة. حين يُبلَّغ عن خطأ:
- اكتب اختبارًا يعيد إنتاجه، وشاهده يفشل. الآن تعرف أنك فهمت الخطأ.
- أصلح الكود حتى ينجح الاختبار.
- احتفظ بالاختبار. لن يعود ذلك الخطأ بصمت أبدًا.
ينجح هذا حتى في قواعد الكود التي لم تُكتب قط بالاختبار أولًا، ويحوّل كل تقرير خطأ إلى تحسين دائم لمجموعة الاختبارات.
متى لا تستخدمه
يفترض الـ TDD أنك تعرف معنى “الصحيح”. وحين لا تعرفه، فكتابة الاختبارات أولًا مجرّد تدوين لتخمينات:
- العمل الاستكشافي على الواجهة: تجربة التخطيطات والتفاعلات والنصوص. ابنِها، وانظر إليها، واعرضها على أحد؛ ثم اختبر ما يبقى منها.
- التجارب المؤقّتة (spikes): تجربة تُرمى بعد استخدامها لتعرف إن كان نهج ما ينجح. احذفها، ثم ابنِ الشيء الحقيقي (بالاختبار أولًا إن شئت).
- كود الربط الخالي من المنطق: وصل دالة راجعة في مكتبة بالـ state لديك. غطِّه باختبار integration أو end-to-end بدلًا من ذلك.
في التمرين تؤدّي النصف الأخضر من الدورة: اختبارات settleUp مكتوبة (وهي الفحوص)، وأنت تكتب الكود الذي يجعلها تنجح. وفي الدرسين القادمين، ستقيس مدى جودة تلك الاختبارات.
أهم النقاط
- Red ثم green ثم refactor: اكتب اختبارًا واحدًا فاشلًا، واجعله ينجح بأبسط كود، ثم نظّف الكود والاختبارات خضراء.
- رؤية الاختبار يفشل أولًا تثبت أنه قادر على الفشل؛ أما الاختبار المكتوب بعد الكود فقد ينجح لسبب خاطئ.
- اختر الاختبار التالي بحيث يفرض خطوة صغيرة واحدة من السلوك الجديد، بادئًا من أبسط حالة.
- يتألّق الـ TDD مع المنطق ذي القواعد الواضحة ومع إصلاح الأخطاء (أعد إنتاج الخطأ باختبار فاشل أولًا)؛ ولا يناسب العمل الاستكشافي على الواجهة ولا التجارب المؤقّتة.