الاختبار المجهول.
طريقة اختبار تقوم بتشغيل الكود ولكنها لا تحتوي على أي تحقق (assertion)، وبالتالي تنجح طالما لم يتم إلقاء أي استثناء — مما يترك الغرض الفعلي منها وما تتحقق منه مجهولاً.
##العلامات والأعراض
اختبار يقوم بإعداد الكائنات واستدعاء النظام الخاضع للاختبار، ثم يتوقف دون وجود تحقق واحد. ينجح الاختبار ببساطة لأنه لم يتم إلقاء أي استثناء — وليس لأنه تم تأكيد أي سلوك متوقع. لا يوجد شيء في جسم الاختبار يوضح كيف تبدو النتيجة "الصحيحة".
العلامات الدالة:
- لا يوجد
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 ولكنه لا يتحقق أبداً من قيمة الإرجاع أو من التفاعلات مع كائنات المحاكاة.
##أسباب المشكلة
لماذا يحدث ذلك
- تم إنشاء هيكل اختبار مؤقت/للمهام المستقبلية (TODO) ("جعله يترجم ويعمل") ولم يتم ملء التحقق أبداً.
- تم إرسال (commit) كود تصحيح أخطاء مؤقت — مثل
console.logأو تشغيل يدوي للتحقق — كما لو كان اختباراً حقيقياً. - أدت عملية إعادة الهيكلة إلى حذف التحقق أو كتابته كتعليق مع ترك كود الإعداد (setup) كما هو.
- يتم الخلط بين عبارة "يعمل دون استثناء" وعبارة "يعمل بشكل صحيح". اختبارات الدخان (Smoke tests) مشروعة، ولكن هنا لا يتم إعلان النية لإجراء اختبار الدخان بشكل صريح.
- وجود هياكل اختبار تم إنشاؤها تلقائياً أو بواسطة الذكاء الاصطناعي تقوم بتشغيل دالة دون التحقق من النتيجة.
لماذا يضر ذلك
- الثقة الزائفة. يساهم الاختبار في نجاح الاختبارات (الشريط الأخضر) وفي تغطية السطور/الفروع (coverage)، ومع ذلك لا يتحقق من شيء. تكذب مقاييس التغطية بنشاط حول مدى حماية الكود.
- لا حماية من الأخطاء التراجعية (regression). يمكن أن ينكسر السلوك بصمت — قيمة إرجاع خاطئة، حالة خاطئة — ومع ذلك تظل مجموعة الاختبارات ناجحة. هذا هو أسوأ أنواع الاختبارات: فهو يكلف صيانة دون أن يكتشف شيئاً.
- الغرض مجهول. لا يمكن للقارئ (أو المطور المسؤول عن الصيانة مستقبلاً) معرفة السلوك المضمون، لذا لا يمكنهم تغيير الكود أو الاختبار بأمان. إنه لا يوثق أي شيء.
- يقوض الثقة في مجموعة الاختبارات. بمجرد أن يلاحظ المطورون وجود اختبارات لا تختبر شيئاً بالفعل، يتوقفون عن الاعتقاد بأن النجاح (الشريط الأخضر) يعني الجودة — مما يقوض قيمة الاختبارات الأخرى أيضاً.
إنه الصورة المعاكسة لـ روليت التحققات (Assertion Roulette): ففي تلك المشكلة توجد تحققات أكثر من اللازم وغير موثقة؛ بينما لا يحتوي الاختبار المجهول (Unknown Test) على أي تحقق.
##المعالجة
اجعل كل اختبار يصرح بما يتوقعه، واجعل عبارة "لا ينبغي إلقاء أي استثناء" خياراً صريحاً ومقصوداً.
- أضف تحققاً واحداً على الأقل على نتيجة ملاحظة — مثل قيمة الإرجاع، أو الحالة الناتجة، أو خطأ تم إلقاؤه. واستبدل طباعة كود تصحيح الأخطاء بـ
expectعلى تلك القيمة. - إذا كانت النية الحقيقية هي "يجب ألا يتم إلقاء استثناء"، فاذكر ذلك صراحة باستخدام
expect(() => fn()).not.toThrow()(وللدالة غير المتزامنة:await expect(fn()).resolves.not.toThrow()؛ إذ سيفشل.resolves.toBeDefined()إذا انتهت الدالة إلى القيمةundefined). وبهذا يصبح اختبار الدخان موثقاً بدلاً من أن يكون عرضياً. - للاختبارات القائمة على التفاعل فقط، تحقق من الكائن المساعد (collaborator):
expect(mock).toHaveBeenCalledWith(...). - احذف المواضع المؤقتة الميتة أو حوّلها إلى
todoمرتبط بتذكرة متابعة. يظهرit.todo('handles chained deps — PROJ-123')في التقرير بوصفه todo لا بوصفه اختبارًا ناجحًا، فتبقى الفجوة ظاهرة دون تزييف التغطية. لا تترك خلفك اختبارًا فارغًا ينجح، ولا تركن الاختبار بـskipمجرد من أي سبب، فذلك يستبدل بهذه المشكلة اختبارًا متجاهَلًا فحسب. - قم بتفعيل أداة كشف (مثل
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);
});
##أدوات الكشف
- eslint-jest jest/expect-expect — يشترط وجود تحقق (assertion) واحد على الأقل في جسم كل اختبار
- eslint-vitest vitest/expect-expect — يشترط وجود تحقق (assertion) واحد على الأقل في جسم كل اختبار
- sonar javascript:S2699 — يجب أن تتضمن الاختبارات عمليات تحقق (JavaScript)
- sonar java:S2699 — يجب أن تتضمن الاختبارات عمليات تحقق (Java)