اختبار الأرقام السحرية.
يكتب الاختبار قيماً رقمية ثابتة غير مفسرة في مدخلاته وتحققاته، مما يخفي معنى الأرقام ومصدرها.
##Signs and Symptoms
تكتشف اختبار الأرقام السحرية عندما تكون وسائط الاختبار وتحققاته مليئة بالأرقام المجردة التي لا يظهر معناها أو أصلها بوضوح في الكود. ويضطر القارئ إلى إجراء هندسة عكسية (أو مجرد تصديق) سبب توقع قيمة معينة.
العلامات الدالة:
- تظهر القيم الرقمية الثابتة مباشرة كوسائط في عبارات التحقق: مثل
expect(result).toBe(54.13)، وassertEquals(86400, ttl). - تكرار القيمة الثابتة نفسها عبر التهيئة والتنفيذ والقيمة المتوقعة، دون وجود اسم يربط بينها.
- ترميز الأرقام لمفاهيم النطاق (domain concepts) غير الموضحة (مثل
3600= ساعة، و200= نجاح HTTP OK، و0.0825= نسبة الضريبة). - وجود تعليق بجانب الرقم لشرحه — وهي علامة على أنه كان ينبغي تسمية الرقم نفسه.
- يسأل الأشخاص أثناء مراجعة الكود "لماذا
42؟" أو "من أين تأتي54.13؟" ولا يستطيع أحد الإجابة دون إعادة تشغيل الكود.
// رائحة: ما هي 8.25؟ ولماذا 54.13؟ وما هي الـ 50 المخفية داخل الدالة المساعدة؟
test('checkout works', () => {
const total = checkout(cartFor(50), 8.25);
expect(total).toBe(54.13);
});
هذه نكهة محددة في جانب الاختبار من رائحة الأرقام السحرية العامة، وهي مساهم كلاسيكي في رائحة التبعية الصلبة المترابطة (ميسزاروس): حيث لا يمكن للقارئ فهم الاختبار من الاختبار بمفرده.
##Reasons for the Problem
لماذا يحدث ذلك
- القيم الثابتة (literals) هي الطريق الأسهل: تكتب القيمة التي رأيتها في مصحح الأخطاء (debugger) أو تنسخ المخرجات الفعلية من تشغيل فاشل وتضعها في التحقق حتى يتحول للون الأخضر ("تخمين القيمة" / لصق المخرجات).
- يكون سياق العمل (domain context) في ذهن الكاتب بالفعل، لذا تبدو قيم مثل
3600أو8.25بديهية في وقت الكتابة. - يتم اختيار قيم التجهيز عشوائياً (مثل
new User(25, ...)) لمجرد إرضاء الباني، دون التفكير في معناها.
لماذا يضر ذلك
- سهولة القراءة / الغرض. يوضح رقم مثل
54.13حقيقة ولكنه لا يوضح السبب. لا يستطيع المراجعون والمسؤولون عن الصيانة في المستقبل معرفة ما إذا كانت هذه القيمة عبارة عن توقع مقصود، أو حداً، أو مجرد مصادفة. ويتوقف الاختبار عن كونه توثيقاً قابلاً للتنفيذ. - سهولة الصيانة. عندما تتغير القاعدة (نسبة الضريبة، أو مهلة الانتظار، أو حجم الصفحة)، يتعين عليك البحث عن كل نسخة من القيمة الثابتة وتحديد أي رقم
7يعني "أيام" وأيها يعني "أقصى عدد للمحاولات". التكرار غير المسمى يجعل التعديلات الآمنة مكلفة وعرضة للخطأ. - الموثوقية / الثقة الزائفة. إذا كانت القيمة المتوقعة خاطئة — أو صحيحة فقط بمحض الصدفة — فلن يكشف الاختبار ذلك. والأسوأ من ذلك أن المطورين غالباً ما "يصلحون" اختبار الأرقام السحرية عن طريق إعادة حساب القيمة المتوقعة باستخدام صيغة كود الإنتاج (مثل
expect(total).toBe(subtotal * (1 + rate))))، مما يحول التحقق إلى تحصيل حاصل يعيد تنفيذ الكود قيد الاختبار ولا يمكن أن يفشل للسبب الصحيح أبداً. - التشخيص. عندما يتعطل مثل هذا الاختبار، تكون رسالة الفشل مجرد "expected 54.13, got 54.12" دون أي تلميح عن المدخلات أو القاعدة التي أنتجت هذا الرقم، مما يبطئ عملية تصحيح الأخطاء.
##Treatment
طبق استبدال الرقم السحري بثابت رمزي (ميسزاروس): امنح كل قيمة ذات مغزى اسماً يوضح دورها، واجعل العلاقة بين المدخلات والنتيجة المتوقعة صريحة.
خطوات ملموسة:
- سمِّ المدخلات. استخرج القيم الثابتة المستخدمة كبيانات اختبار إلى ثوابت محلية مسماة جيداً أو استدعاءات بناء التجهيز (مثل
const SUBTOTAL = 50.00، وconst TAX_RATE_PCT = 8.25). استخدم Object Mother أو البناء (builder) لتجهيز الكائنات بحيث تظهر القيم التي تهم الاختبار فقط. - سمِّ القيمة المتوقعة واشرحها. احتفظ بالنتيجة المتوقعة كقيمة ثابتة مستقلة، ولكن سمها ووثق كيفية اشتقاقها (مثل
const EXPECTED_TOTAL = 54.13; // SUBTOTAL مضافاً إليه 8.25% ضريبة مبيعات). ولا تعد حسابها باستخدام معادلة كود الإنتاج — لأن ذلك يختبر الكود ضد نفسه فقط. - اربط المدخلات بالتحقق بحيث يتمكن القارئ من التحقق من الحساب بالنظر، أو التحقق من قيمة مرجعية مشتقة ومستقلة.
- اترك القيم البديهية وشأنها. مثل
0، و1، و-1، وفهارس المصفوفات، والأعداد الواضحة (مثلitems.toHaveLength(2))؛ فهذه عادة لا تحتاج إلى أسماء. احتفظ بالثوابت للقيم التي لا يفسر معناها نفسه بنفسه. لا ترفع ثابتاً مشتركاً ليكون مصدراً وحيداً للحقيقة إلا عندما يمثل المفهوم نفسه فعلياً في كل مكان.
// قبل
test('checkout works', () => {
const total = checkout(cartFor(50), 8.25);
expect(total).toBe(54.13);
});
// بعد
const SUBTOTAL = 50.00;
const TAX_RATE_PCT = 8.25;
const EXPECTED_TOTAL = 54.13; // SUBTOTAL مضافاً إليه 8.25% ضريبة مبيعات
test('applies sales tax to the subtotal', () => {
const total = checkout(cartFor(SUBTOTAL), TAX_RATE_PCT);
expect(total).toBe(EXPECTED_TOTAL);
});
تحمل الأسماء الآن الغرض؛ وإذا تغيرت قاعدة الضريبة، يكون التعديل محلياً وواضحاً، وتظل القيمة المتوقعة فحصاً صادقاً ومستقلاً بدلاً من كونه تحصيل حاصل.
##Detected by
- eslint no-magic-numbers — يجب الإعلان عن الأرقام السحرية كثوابت مسماة
- typescript-eslint @typescript-eslint/no-magic-numbers — منع الأرقام السحرية (TypeScript)
- sonar javascript:S109 — يجب عدم استخدام الأرقام السحرية
- checkstyle MagicNumber — MagicNumber
- tsDetect اختبار الأرقام السحرية — اختبار الأرقام السحرية (كاشف مخصص للاختبارات)