الاختبار المجهول.
طريقة اختبار تقوم بتشغيل الكود ولكنها لا تحتوي على أي تحقق (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
اجعل كل اختبار يصرح بما يتوقعه، واجعل عبارة "لا ينبغي إلقاء أي استثناء" خياراً صريحاً ومقصوداً.
- أضف تحققاً واحداً على الأقل على نتيجة ملاحظة — مثل قيمة الإرجاع، أو الحالة الناتجة، أو خطأ تم إلقاؤه. واستبدل طباعة كود تصحيح الأخطاء بـ
expectعلى تلك القيمة. - إذا كانت النية الحقيقية هي "يجب ألا يتم إلقاء استثناء"، فاذكر ذلك صراحة باستخدام
expect(() => fn()).not.toThrow()(أوawait expect(fn()).resolves.toBeDefined()). وبهذا يصبح اختبار الدخان موثقاً بدلاً من أن يكون عرضياً. - للاختبارات القائمة على التفاعل فقط، تحقق من الكائن المساعد (collaborator):
expect(mock).toHaveBeenCalledWith(...). - احذف أو استخدم
skip/todoللمواقع المؤقتة الميتة بدلاً من ترك اختبار فارغ ناجح (يسجلit.todo('handles chained deps')الفجوة بأمانة دون تزييف التغطية). - قم بتفعيل أداة كشف (مثل
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-expect — jest/expect-expect
- eslint-vitest expect-expect — vitest/expect-expect
- sonar S2699 — يجب أن تتضمن الاختبارات عمليات تحقق (JavaScript)
- sonar S2699 — يجب أن تتضمن الاختبارات عمليات تحقق (Java)