منطق الاختبار في كود الإنتاج.
يحتوي كود الإنتاج على منطق أو فروع أو أعضاء موجودة فقط لدعم عملية الاختبار، مما يؤدي إلى عدم وضوح الحدود بين ما يتم تسليمه فعلياً للعملاء وما يتم اختباره فقط.
##Signs and Symptoms
تجد كوداً في النظام الخاضع للاختبار (SUT) يهم فقط عندما يتم تشغيل الاختبار. العلامات الدالة:
- خطافات الاختبار / علامات الوضع (Test hooks / mode flags) — فروع تعتمد على علامات مثل
testing، أوisTest، أوNODE_ENV === 'test'، أوmockوالتي تتجاوز السلوك الحقيقي. - أعضاء "للاختبار فقط" — دوال setter أو getter أو
reset()عامة أو دوال بناء مضافة فقط لتمكين الاختبار من الوصول إلى الحالة الداخلية، وغالباً ما يتم وسمها بـ@VisibleForTesting/@TestOnlyوتستدعى بالفعل في كود الإنتاج. - تلوث المساواة (Equality pollution) — إضافة منطق
equals()أو منطق المقارنة إلى فئة إنتاجية لمجرد تمكين التحقق من مقارنة كائنين. - اعتماد الإنتاج على الاختبار — قيام وحدات الإنتاج باستيراد إطار عمل اختبار أو هيكل اختبار أو مصنع كائنات محاكاة (mock factory).
// مشكلة (SMELL): يتصرف الكود المشحون بشكل مختلف عند تفعيل وضع "الاختبار"
class PaymentService {
charge(order: Order) {
if (process.env.NODE_ENV === 'test' || this.isTesting) {
return { status: 'ok', id: 'FAKE-TEST-ID' }; // لا يتم أبداً تشغيل بوابة الدفع الحقيقية
}
return this.gateway.charge(order); // <-- المسار الذي يتم شحنه وتشغيله فعلياً
}
}
الفرع "الحقيقي" هو الفرع الذي يصل إليه عملاؤك، وهو بالضبط الفرع الذي تتجاهله اختباراتك.
##Reasons for the Problem
لماذا يحدث ذلك
- يكون النظام الخاضع للاختبار (SUT) صعب الاختبار (يتواصل مع شبكة، أو ساعة النظام، أو بوابة دفع، أو نظام ملفات) وتكون إضافة اختصار
if (testing)أسرع من تقديم واجهة فصل مناسبة (proper seam). - يحتاج الاختبار إلى مراقبة أو ضبط الحالة الداخلية، لذا يقوم المطور بإتاحتها "من أجل الاختبار فقط".
- تكون محاكاة الكائنات (mocking/stubbing) صعبة أو غير مريحة، لذا يتم ترميز بيانات جاهزة مسبقاً (canned data) خلف علامة (flag).
لماذا يضر ذلك
- الثقة الزائفة. تقوم الاختبارات بتشغيل فرع الاختبار فقط، وبالتالي يتم شحن مسار الإنتاج بدون اختبار. نجاح الاختبار يثبت أن الكود المزيف يعمل، وليس الكود الفعلي.
- الموقوفية / الأمان. قد يتم تشغيل الكود المخصص للاختبار فقط في بيئة الإنتاج الفعلية بالخطأ. القصة الكلاسيكية التحذيرية التي يذكرها "ميسزاروس" هي صاروخ Ariane 5: حيث تُرك كود مخصص للعمل الأرضي فقط نشطاً أثناء الرحلة مما تسبب في الفشل. إن ترك شرط
if (isTesting)مفعلاً في كود الإنتاج هو المكافئ البرمجي لذلك الخطأ. - الأمن البرمجي. تجاوزات الاختبار هي بمثابة أبواب خلفية (backdoors) — فعلامة تتخطى المصادقة أو الدفع أو التحقق لا يفصلها عن الاستغلال الأمني سوى خطأ واحد في التهيئة والإعداد.
- المقروئية وتضخم واجهة برمجة التطبيقات (API). تزيد دوال set/get أو
reset()المخصصة للاختبار فقط من مساحة السطح العام للفئة وتضلل المستخدمين الفعليين حول الغرض الأساسي من الفئة. - قابلية الصيانة. يعيش سلوكان مختلفان في فئة واحدة؛ ومع كل تغيير يجب التفكير في كل من مسار الإنتاج ومسار الاختبار، ويتعفن هذا الاختلاف بصمت مع مرور الوقت.
##Treatment
أخرج منطق الاختبار من كود الإنتاج من خلال تقديم واجهة فصل مناسبة (seam) بدلاً من استخدام العلامات (flags).
- احقن التغيير (حقن التبعية + بديل الاختبار). استبدل الفرع المشفر بزميل (collaborator) يستبدله الاختبار. يقوم كود الإنتاج بتوصيل التطبيق الحقيقي؛ بينما يقوم الاختبار بتوصيل كائن مزيف/بديل/محاكى.
- استخدم فئة فرعية مخصصة للاختبار (Test-Specific Subclass) عندما تحتاج فقط إلى تجاوز دالة واحدة — وتجاوزها في فئة فرعية تعيش في كود الاختبار، وليس عبر شرط
ifفي الفئة الأساسية. - طبق نمط الكائن المتواضع (Humble Object pattern) لسحب المنطق الذي يصعب اختباره (مثل الساعة، أو عمليات الإدخال/الإخراج) خلف محول رفيع (thin adapter)، بحيث يصبح المنطق الأساسي قابلاً للاختبار مباشرة دون الحاجة لخطافات.
- انقل منطق المقارنة إلى جانب الاختبار. بدلاً من تلويث كود الإنتاج بـ
equals()من أجل التحققات، استخدم أداة مطابقة/مقارنة مخصصة أو تحقق من الحقول التي تهمك فقط. - ابقِ كود الاختبار خارج عملية البناء (build). استخدم الفصل بين مجموعات المصادر/تكوين البناء بحيث لا يمكن تجميع المساعدين في الملف المشحون النهائي؛ وميز الأعضاء المرئية للاختبار بـ
@VisibleForTesting/@TestOnlyودع أداة التحليل الإملائي (linter) تفرض عدم استدعاء كود الإنتاج لها أبداً.
// قبل: خطاف اختبار داخل كود الإنتاج
class PaymentService {
charge(order: Order) {
if (this.isTesting) return { status: 'ok', id: 'FAKE-TEST-ID' };
return this.gateway.charge(order);
}
}
// بعد: مسار كود واحد؛ يتم حقن بوابة الدفع ومحاكاتها في الاختبار
class PaymentService {
constructor(private gateway: PaymentGateway) {}
charge(order: Order) {
return this.gateway.charge(order); // نفس مسار الكود في الإنتاج والاختبار
}
}
// اختبار
const fakeGateway = { charge: () => ({ status: 'ok', id: 'FAKE-TEST-ID' }) };
const service = new PaymentService(fakeGateway);
الآن مسار الإنتاج هو المسار الوحيد، ويتحكم الاختبار في السلوك من الخارج.
##Detected by
- codeql java/visible-for-testing-abuse — استخدام VisibleForTesting في كود الإنتاج
- deepsource JAVA-A1067 — لا ينبغي استخدام دوال @VisibleForTesting/@TestOnly في كود غير مخصص للاختبار
- android-lint VisibleForTests — مرئي للاختبارات فقط (Visible Only For Tests)