ConstructiCat Logo
CodeBust.
Browse section ▾

الاختبار المجهول.

طريقة اختبار تقوم بتشغيل الكود ولكنها لا تحتوي على أي تحقق (assertion)، وبالتالي تنجح طالما لم يتم إلقاء أي استثناء — مما يترك الغرض الفعلي منها وما تتحقق منه مجهولاً.

##Signs and Symptoms

اختبار يقوم بإعداد الكائنات واستدعاء النظام الخاضع للاختبار، ثم يتوقف دون وجود تحقق واحد. ينجح الاختبار ببساطة لأنه لم يتم إلقاء أي استثناء — وليس لأنه تم تأكيد أي سلوك متوقع. لا يوجد شيء في جسم الاختبار يوضح كيف تبدو النتيجة "الصحيحة".

العلامات الدالة:

  • لا يوجد expect أو assert أو verify في أي مكان في جسم الاختبار.
  • يتم "التحقق" عن طريق استخدام console.log أو طباعة النتيجة والتي يفترض أن يقوم الإنسان بفحصها بالنظر.
  • اسم اختبار غامض (مثل testChainDependencies) وجسم لا يعطي أي إشارة إلى ما يضمنه الاختبار.
  • سيظل الاختبار ناجحاً حتى لو أعادت الدالة الإنتاجية قيمة خاطئة تماماً.
  • الاختبارات التي تعتمد بالكامل على عبارة "لم يتم إلقاء استثناء" دون ذكر ذلك صراحة.
// مشكلة (Smell): يشغل الكود، يطبع، لا يتحقق من شيء — ينجح بغض النظر عما تعيده calculate() 
test('chain dependencies', () => {
  const game = Game.newGame(0, '');
  game.setOtherGoods(Building.TOOLMAKERS, 1);
  const logic = new Logic(game);

  const res = logic.calculateChainWithDependencies(Goods.TOOLS);
  console.log(res.toString()); // لا يوجد expect(...) — ما الذي يفحصه هذا الاختبار؟
});

يختبئ شكل شائع من هذه المشكلة خلف المحاكاة المكثفة (heavy mocking): حيث يقوم الاختبار بتوصيل كائنات المحاكاة واستدعاء الـ SUT ولكنه لا يتحقق أبداً من قيمة الإرجاع أو من التفاعلات مع كائنات المحاكاة.

##Reasons for the Problem

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

  • تم إنشاء هيكل اختبار مؤقت/للمهام المستقبلية (TODO) ("جعله يترجم ويعمل") ولم يتم ملء التحقق أبداً.
  • تم إرسال (commit) كود تصحيح أخطاء مؤقت — مثل console.log أو تشغيل يدوي للتحقق — كما لو كان اختباراً حقيقياً.
  • أدت عملية إعادة الهيكلة إلى حذف التحقق أو كتابته كتعليق مع ترك كود الإعداد (setup) كما هو.
  • يتم الخلط بين عبارة "يعمل دون استثناء" وعبارة "يعمل بشكل صحيح". اختبارات الدخان (Smoke tests) مشروعة، ولكن هنا لا يتم إعلان النية لإجراء اختبار الدخان بشكل صريح.
  • وجود هياكل اختبار تم إنشاؤها تلقائياً أو بواسطة الذكاء الاصطناعي تقوم بتشغيل دالة دون التحقق من النتيجة.

لماذا يضر ذلك

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

إنه الصورة المعاكسة لـ روليت التحققات (Assertion Roulette): ففي تلك المشكلة توجد تحققات أكثر من اللازم وغير موثقة؛ بينما لا يحتوي الاختبار المجهول (Unknown Test) على أي تحقق.

##Treatment

اجعل كل اختبار يصرح بما يتوقعه، واجعل عبارة "لا ينبغي إلقاء أي استثناء" خياراً صريحاً ومقصوداً.

  1. أضف تحققاً واحداً على الأقل على نتيجة ملاحظة — مثل قيمة الإرجاع، أو الحالة الناتجة، أو خطأ تم إلقاؤه. واستبدل طباعة كود تصحيح الأخطاء بـ expect على تلك القيمة.
  2. إذا كانت النية الحقيقية هي "يجب ألا يتم إلقاء استثناء"، فاذكر ذلك صراحة باستخدام expect(() => fn()).not.toThrow() (أو await expect(fn()).resolves.toBeDefined()). وبهذا يصبح اختبار الدخان موثقاً بدلاً من أن يكون عرضياً.
  3. للاختبارات القائمة على التفاعل فقط، تحقق من الكائن المساعد (collaborator): expect(mock).toHaveBeenCalledWith(...).
  4. احذف أو استخدم skip/todo للمواقع المؤقتة الميتة بدلاً من ترك اختبار فارغ ناجح (يسجل it.todo('handles chained deps') الفجوة بأمانة دون تزييف التغطية).
  5. قم بتفعيل أداة كشف (مثل jest/expect-expect، أو vitest/expect-expect، أو SonarSource S2699) في بيئة الـ CI. وإذا قمت بلف التحققات في دوال مساعدة مخصصة، فقم بتسجيلها عبر خيار assertFunctionNames الخاص بالقاعدة حتى لا يتم الإبلاغ عن التحققات الحقيقية.
// قبل — اختبار مجهول
test('chain dependencies', () => {
  const logic = new Logic(Game.newGame(0, ''));
  const res = logic.calculateChainWithDependencies(Goods.TOOLS);
  console.log(res.toString());
});

// بعد — النية والضمان صريحان
test('resolves tools to the toolmakers workshop chain', () => {
  const logic = new Logic(Game.newGame(0, ''));
  const res = logic.calculateChainWithDependencies(Goods.TOOLS);
  expect(res).toHaveLength(1);
  expect(res[0].building).toBe(Building.TOOLMAKERS);
});

##Detected by

  • eslint-jest expect-expectjest/expect-expect
  • eslint-vitest expect-expectvitest/expect-expect
  • sonar S2699يجب أن تتضمن الاختبارات عمليات تحقق (JavaScript)
  • sonar S2699يجب أن تتضمن الاختبارات عمليات تحقق (Java)