EduFlow
اختبار تطبيقات JavaScript
⁦0%⁩

الوحدة 5 · استراتيجية الاختبار

التطوير الموجّه بالاختبارات كأداة

20 دقراءة 4 دبيئة تجريب الكود

الدرس في لمحة

تستخدم دورة red-green-refactor لبناء منطق محدّد المواصفات ولإصلاح الأخطاء، وتميّز الحالات التي تبطّئك فيها كتابة الاختبارات أولًا.

  1. 1الدورة
  2. 2بناء settleUp بكتابة الاختبار أولًا
  3. 3إصلاح الأخطاء بكتابة الاختبار أولًا
  4. 4متى لا تستخدمه

لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.

للتطوير الموجّه بالاختبارات (Test-driven development أو TDD) مشكلة في سمعته. بعض الناس يعاملونه كدين؛ وآخرون يرفضونه بعد تمرين واحد مؤلم. أنا أعامله كأداة، ومثل أي أداة فهو ممتاز لبعض المهام وخاطئ لأخرى. أما بالنسبة إلى منطق المال الأساسي في Splitwise-lite، فهو أفضل أداة أعرفها.

الدورة

الـ TDD حلقة قصيرة، تستغرق عادةً دقيقة أو اثنتين في كل جولة:

  1. Red (الأحمر): اكتب اختبارًا صغيرًا واحدًا لسلوك غير موجود بعد. شغّله. وشاهده يفشل، ويُفضَّل أن يكون بالفشل الذي توقّعته.
  2. Green (الأخضر): اكتب أبسط كود يجعله ينجح. بسيط، لا ذكي؛ والتثبيت الصريح للقيم (hard-coding) مسموح به إن كان صادقًا.
  3. Refactor (إعادة الهيكلة): نظّف الكود (والاختبارات) وكل شيء يبقى أخضر.
دورة من ثلاثة صناديق: الأحمر، اكتب اختبارًا فاشلًا؛ الأخضر، اجعله ينجح؛ إعادة الهيكلة، نظّف والاختبارات خضراء؛ ثم العودة إلى الأحمر. Red اختبار فاشل Green أبسط كود ناجح Refactor نظّف، وابقَ أخضر
Red ثم green ثم refactor

لماذا الإصرار على رؤية الأحمر؟ لأن الاختبار الذي لم تره يفشل قط قد لا يكون قادرًا على الفشل. قد يستدعي الدالة الخاطئة، أو يؤكّد متغيّرًا قيمته truthy دائمًا، أو لا يفحص شيئًا في الحقيقة (await مفقودة، أو .skip منسيّة في مكانها). الأحمر أولًا دليل رخيص على أن الاختبار موصول بشكل صحيح.

بناء settleUp بكتابة الاختبار أولًا

يحتاج Splitwise-lite إلى settleUp(balances): بمعرفة من له ومن عليه (بالسنتات)، تعيد قائمة قصيرة من التحويلات تعيد الجميع إلى الصفر. إليك كيف تسير الجولات الأولى، مع Vitest في وضع المراقبة كي يعيد كل حفظ تشغيل الاختبارات.

الجولة 1، أبسط حالة. لا أحد مدين بشيء:

src/settle-up.test.js
test('needs no transfers when everyone is settled', () => {
  expect(settleUp({ Ana: 0, Ben: 0 })).toEqual([]);
});

الأحمر: settleUp is not defined. الأخضر: export function settleUp() { return []; }. نعم، هذا هو التطبيق كله في الوقت الحالي. إنه صادق بشأن ما تطلبه الاختبارات.

الجولة 2، دين واحد. يفرض الاختبار التالي سلوكًا حقيقيًا:

JavaScript
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 لا علاقة له بالميزات الجديدة. حين يُبلَّغ عن خطأ:

  1. اكتب اختبارًا يعيد إنتاجه، وشاهده يفشل. الآن تعرف أنك فهمت الخطأ.
  2. أصلح الكود حتى ينجح الاختبار.
  3. احتفظ بالاختبار. لن يعود ذلك الخطأ بصمت أبدًا.

ينجح هذا حتى في قواعد الكود التي لم تُكتب قط بالاختبار أولًا، ويحوّل كل تقرير خطأ إلى تحسين دائم لمجموعة الاختبارات.

متى لا تستخدمه

يفترض الـ TDD أنك تعرف معنى “الصحيح”. وحين لا تعرفه، فكتابة الاختبارات أولًا مجرّد تدوين لتخمينات:

  • العمل الاستكشافي على الواجهة: تجربة التخطيطات والتفاعلات والنصوص. ابنِها، وانظر إليها، واعرضها على أحد؛ ثم اختبر ما يبقى منها.
  • التجارب المؤقّتة (spikes): تجربة تُرمى بعد استخدامها لتعرف إن كان نهج ما ينجح. احذفها، ثم ابنِ الشيء الحقيقي (بالاختبار أولًا إن شئت).
  • كود الربط الخالي من المنطق: وصل دالة راجعة في مكتبة بالـ state لديك. غطِّه باختبار integration أو end-to-end بدلًا من ذلك.

في التمرين تؤدّي النصف الأخضر من الدورة: اختبارات settleUp مكتوبة (وهي الفحوص)، وأنت تكتب الكود الذي يجعلها تنجح. وفي الدرسين القادمين، ستقيس مدى جودة تلك الاختبارات.

أهم النقاط

  • Red ثم green ثم refactor: اكتب اختبارًا واحدًا فاشلًا، واجعله ينجح بأبسط كود، ثم نظّف الكود والاختبارات خضراء.
  • رؤية الاختبار يفشل أولًا تثبت أنه قادر على الفشل؛ أما الاختبار المكتوب بعد الكود فقد ينجح لسبب خاطئ.
  • اختر الاختبار التالي بحيث يفرض خطوة صغيرة واحدة من السلوك الجديد، بادئًا من أبسط حالة.
  • يتألّق الـ TDD مع المنطق ذي القواعد الواضحة ومع إصلاح الأخطاء (أعد إنتاج الخطأ باختبار فاشل أولًا)؛ ولا يناسب العمل الاستكشافي على الواجهة ولا التجارب المؤقّتة.

قراءات إضافية

تحقّق من فهمك

3 أسئلة

السؤال 1 من 3

تكتب اختبارًا لحالة جديدة فينجح فورًا، قبل أن تغيّر أي كود. ماذا يجب أن تفعل؟

السؤال 2 من 3

يبلّغ مستخدم أن تسوية حسابات مجموعة يدين فيها شخص واحد بمبلغ $0.01 تؤدّي إلى انهيار. ما طريقة الـ TDD لإصلاح ذلك؟

السؤال 3 من 3

أيّ مهمّة هي الأقل ملاءمة للتطوير الصارم القائم على كتابة الاختبار أولًا؟

تدريب

بيئة تجريب الكود

اجعل اختبارات settleUp تنجح

هذه هي خطوة الأخضر في الـ TDD: الاختبارات مكتوبة أصلًا (وهي الفحوص أدناه)، ومهمّتك أن تجعلها تنجح.

تأخذ settleUp(balances) أرصدة بالسنتات ({ Ana: 1500, Ben: -1000, Cai: -500 }: الموجب يعني أن للشخص مالًا، والسالب يعني أن عليه مالًا) وتعيد تحويلات مثل [{ from: 'Ben', to: 'Ana', amountCents: 1000 }, { from: 'Cai', to: 'Ana', amountCents: 500 }].

تشترط الاختبارات ما يلي: المجموعات المسوّاة لا تحتاج إلى تحويلات؛ والمدين الواحد يدفع للدائن الواحد؛ وتطبيق التحويلات يعيد الجميع إلى الصفر بمبالغ موجبة من سنتات صحيحة؛ وعدد التحويلات لا يتجاوز عدد الأشخاص − 1 (بعدّ من لهم رصيد غير صفري)؛ ومن سوّوا حساباتهم لا يظهرون أبدًا؛ والأرصدة التي لا يساوي مجموعها صفرًا ترمي خطأً؛ وكائن المدخلات لا يُعدَّل.

اعمل على الفحوص بالترتيب، وشغّل بعد كل تغيير. الكود المبدئي ينجح في الفحص الأول.

// Make the tests (the checks) pass, one at a time.
// balances: { name: cents }. Positive = is owed money, negative = owes.
// Return transfers: [{ from, to, amountCents }].
function settleUp(balances) {
  return [];
}

console.log(settleUp({ Ana: 500, Ben: -500 }));
جارٍ تحميل المحرّر…

Ctrl/⌘ + Enter للتشغيل · يعمل في بيئة معزولة داخل متصفّحك. لا شيء يغادر جهازك.

الـ console

(لا مخرجات)

الفحوص

  • المجموعات المسوّاة (والفارغة) لا تحتاج إلى تحويلات
  • مع شخصين، يدفع المدين للدائن المبلغ كاملًا
  • تطبيق التحويلات يعيد الجميع إلى الصفر، بمبالغ موجبة من سنتات صحيحة
  • لا يتجاوز عدد التحويلات عدد الأشخاص − 1
  • من رصيدهم صفر لا يظهرون أبدًا في أي تحويل
  • ترمي خطأً حين لا يساوي مجموع الأرصدة صفرًا
  • لا تعدّل كائن الأرصدة الذي استلمته