ConstructiCat Logo
CodeBust.
Browse section ▾

منطق الاختبار الشرطي.

اختبار يستخدم `if`/`switch`/ternaries أو الحلقات أو `try`/`catch` لتحديد ما سيتم تشغيله أو التحقق منه، بحيث يعتمد سلوكه — وما إذا كان يتحقق من أي شيء على الإطلاق — على الفرع الذي يتم تنفيذه في وقت التشغيل.

##Signs and Symptoms

يبدو الاختبار وكأنه برنامج صغير بدلاً من أن يكون سيناريو خطياً يعتمد على "التجهيز → التنفيذ → التحقق" (arrange → act → assert). ابحث عن تدفق التحكم داخل جسم الاختبار:

  • استخدام if/else، أو switch، أو التعبيرات الشرطية الثلاثية، أو الاختصارات باستخدام &&/|| والتي تحدد أي من التحققات سيتم تشغيله.
  • حلقات for/while/forEach التي تبني المدخلات أو تكرر التحققات.
  • استخدام try/catch لـ "اختبار" مسار الخطأ، مع إخفاء استدعاءات expect داخل catch.
  • إعادة استخدام اختبار واحد لعدة حالات عن طريق التفرع بناءً على علامة أو حالة البيئة.
  • حساب القيم المتوقعة داخل الاختبار (غالباً باستخدام حلقة أو معادلة) بدلاً من كتابتها بشكل ثابت.
// رائحة: توجد التحققات داخل كود شرطي/متفرع
test('user discount', () => {
  const user = getUser();
  if (user.isPremium) {
    expect(price(user)).toBe(80);   // قد لا يتم تشغيله أبداً
  } else {
    expect(price(user)).toBe(100);  // قد لا يتم تشغيله أبداً
  }
});

test('throws on bad input', () => {
  try {
    parse('!!!');
    // إذا لم تقم parse() بإلقاء خطأ، فسننتقل للسطر التالي دون التحقق من شيء ← ينجح الاختبار
  } catch (err) {
    expect(err.message).toMatch(/invalid/);
  }
});

وضع الفشل الدال: يظل الاختبار باللون الأخضر (ناجحاً) حتى عندما يكون الكود معطلاً، لأن الفرع الذي يحتوي على التحقق لم يتم الدخول إليه مطلقاً.

##Reasons for the Problem

لماذا يحدث ذلك

  • المبالغة في تطبيق مبدأ DRY (عدم التكرار). يحاول المطورون تغطية عدة سيناريوهات باختبار واحد "مرن"، متفرعين بناءً على المدخلات أو علامة (flag) بدلاً من كتابة اختبار واحد لكل حالة (ميسزاروس: الاختبار المرن).
  • الاقتران بالبيئة. لم يتم فصل النظام تحت الاختبار (SUT) عن تبعياته، لذا يتكيف الاختبار مع أي حالة يجدها في وقت التشغيل.
  • التفكيك الدفاعي. يتسلل استخدام if (resource) resource.close() لتجنب تفكيك التجهيزات التي قد لا تكون موجودة (التفكيك المعقد).
  • التوقعات المحسوبة. يتم اشتقاق النتيجة المتوقعة بنفس خوارزمية كود الإنتاج، مما يسحب هذا المنطق — بالحلقات وكل شيء — إلى الاختبار (منطق الإنتاج في الاختبار).
  • اختبار مسار الخطأ يدوياً. يتم استخدام try/catch للتحقق من خطأ تم إلقاؤه بدلاً من استخدام مطابق مدمج.

لماذا يضر ذلك

  • الثقة الزائفة (الجزء الأسوأ). معظم بيئات التشغيل تفشل الاختبار فقط عندما يلقي التحقق خطأً. إذا تم تخطي فرع التحقق — أو لم يلق الكود قيد الاختبار خطأً داخل try — ينجح الاختبار دون التحقق من أي شيء.
  • كود اختبار غير مختبر. الفروع والحلقات في الاختبار هي منطق ليس له اختبارات خاصة به؛ وبالتالي فإن أي خلل في تدفق التحكم الخاص بالاختبار يمر دون أن يلاحظه أحد.
  • ضعف التشخيص. عندما يفشل اختبار متفرع، يجب عليك أولاً تحديد أي مسار قد تم تشغيله قبل أن تتمكن من تفسير الفشل.
  • صعوبة القراءة والصيانة. يوثق الاختبار الخطي سلوكاً واحداً بنتيجة متوقعة واحدة؛ أما الاختبار المتفرع فيجبر القارئ على محاكاة التنفيذ لمعرفة ما هو مضمون فعلياً.
  • الهشاشة. الاختبارات التي تعتمد على حالة وقت التشغيل أو البيئة تنجح أو تفشل بشكل غير حتمي.

##Treatment

اجعل كل اختبار مساراً خطياً واحداً وغير مشروط. بشكل ملموس:

  1. سيناريو واحد لكل اختبار. قسّم الاختبار المتفرع إلى اختبارات منفصلة، أو استخدم واجهة برمجة التطبيقات المعتمدة على البيانات في إطار العمل (مثل test.each، أو it.each، أو الاختبارات البارامترية) بحيث تكون كل حالة بمثابة تشغيل مستقل مسمى بوضوح ويتم الإبلاغ عنه بشكل مستقل.
  2. ارفع التفرع خارج الاختبار. إذا كانت الحالة تنطبق فقط تحت شرط معين، فحدد ذلك في وقت التعريف (على سبيل المثال: اختيار describe/it بناءً على الإعدادات)، وليس داخل جسم الاختبار — وبذلك تظل التحققات نفسها غير مشروطة.
  3. اكتب القيم المتوقعة بشكل ثابت (Hard-code). استبدل التوقعات المحسوبة بنتائج متوقعة صريحة (أو كائن متوقع Expected Object / مطابِق مخصص). لا تعِد كتابة منطق الإنتاج داخل الاختبار.
  4. اختبر مسارات الأخطاء باستخدام المطابقات، وليس try/catch: مثل expect(fn).toThrow(...)، أو await expect(p).rejects.toThrow(...). فهذه تفشل بوضوح إذا لم يتم إلقاء أي خطأ.
  5. إذا كان التحقق الشرطي لا مفر منه حقاً، فثبت العدد باستخدام expect.assertions(n) / expect.hasAssertions() بحيث يفشل الفرع الذي تم تخطيه بدلاً من النجاح الصامت.
  6. استبدل التفكيك الشرطي بخطافات دورة حياة إطار العمل (afterEach) والتنظيف التلقائي/المتطابق (idempotent)، بحيث لا تكون هناك حاجة لاستخدام if لحماية عملية التفكيك.
// قبل — اختبار متفرع، قد يتم تخطي التحققات
test('user discount', () => {
  const user = getUser();
  if (user.isPremium) expect(price(user)).toBe(80);
  else                expect(price(user)).toBe(100);
});

// بعد — حالة صريحة واحدة لكل صف، كل تحقق يتم تشغيله دائماً
test.each([
  ['premium', { isPremium: true },  80],
  ['regular', { isPremium: false }, 100],
])('price for %s user', (_label, user, expected) => {
  expect(price(user)).toBe(expected);
});

// قبل — try/catch ينجح عندما لا يتم إلقاء أي شيء
try { parse('!!!'); } catch (e) { expect(e.message).toMatch(/invalid/); }

// بعد — يفشل إذا لم تقم parse() بإلقاء خطأ
expect(() => parse('!!!')).toThrow(/invalid/);

##Detected by