كود صعب الاختبار.
كود الإنتاج الذي يجبر تصميمه (الاقتران الوثيق، أو التبعيات المخفية، أو الحالة العامة، أو الإدخال/الإخراج غير الحتمي، أو الواجهات غير المتزامنة فقط) الاختبارات على القيام بالتواءات محرجة، أو يجعل اختبار الوحدة بشكل معزول أمراً مستحيلاً تماماً.
##Signs and Symptoms
تعيش هذه الرائحة في كود الإنتاج، ولكنك تكتشفها أثناء كتابة الاختبارات. العلامة الدالة هي أنه لا يمكن كتابة اختبار وحدة بشكل نظيف — حيث يتعين على الاختبار محاربة التصميم لوضع الكود قيد الاختبار في حالة معروفة وملاحظة النتيجة.
راقب هذه الأعراض:
- كتل ترتيب (arrange) عملاقة. يجب عليك بناء شبكة من المتعاونين لمجرد إنشاء فئة الكود قيد الاختبار ("لا يمكنني اختبار
OrderServiceدون ربط قاعدة البيانات والبوابة ومحمل الإعدادات أيضاً"). - لا يوجد شق (seam) لحقن بديل. يستدعي الكود
new ConcreteThing()، أو يصل إلى كائن فردي ساكن (Database.getInstance()), أو يلمس الشبكة/نظام الملفات/الساعة مباشرة، لذا لا يوجد مكان لاستبدالها ببديل (stub or fake). - عدم الحتمية مضمن في الكود. يعتمد المنطق على
Date.now()، أوMath.random()، أوprocess.env، أو توقيت الساعة الذي لا يمكن للاختبار التحكم فيه، مما ينتج عنه نتائج متقلبة أو غير قابلة للتكرار. - لا توجد نقطة ملاحظة / لا توجد نقطة تحكم. النتيجة التي تريد التحقق منها مدفونة في حالة خاصة أو تأثير جانبي، لذا تصل الاختبارات إليها عبر الانعكاس (reflection)، أو التحويل (casts)، أو وصولات مخصصة للاختبار فقط.
- واجهة غير متزامنة فقط. الطريقة الوحيدة لتشغيل الكود هي بدء مؤقت/خيط/طابور ثم
sleep()أو التحقق الدوري (polling)، لأن الاكتمال لا يمكن ملاحظته مباشرة أبداً. - تقوم بتغيير كود الإنتاج لمجرد اختباره. مثل تخفيف
privateإلىpublic، أو إضافة خطافات اختبار مثلif (testMode) { ... }، أو استخدام الفئات الفرعية فقط للوصول إلى التفاصيل الداخلية.
// صعب الاختبار: تبعيات مخفية وصلبة، حالة عامة، عدم حتمية
class OrderService {
placeOrder(cart) {
const db = Database.getInstance(); // كائن فردي عام (لا يوجد شق)
const gateway = new StripeGateway(API_KEY); // تبعية ملموسة صلبة -> شبكة حقيقية
const id = Math.random().toString(36).slice(2); // غير حتمي
const charge = gateway.charge(cart.total); // استدعاء HTTP حقيقي داخل الوحدة
db.save({ id, at: Date.now(), charge }); // ساعة غير محقونة
return id;
}
}
// لـ "اختبار وحدة" هذا، يجب عليك الاتصال بـ Stripe، وتعديل Date/Math عالمياً،
// وفحص كائن فردي مشترك — أي إجراء اختبار غير مباشر عبر واجهات محرجة.
##Reasons for the Problem
لماذا يحدث ذلك
- كتابة الاختبارات آخراً على الكود القديم. يسمي ميسزاروس السبب الجذري بافتقار التصميم لقابلية الاختبار. تنشأ قابلية الاختبار بشكل طبيعي من التطوير الموجه بالاختبار (TDD)، ولكن عندما تُكتب الاختبارات في "النهاية"، لا شيء يضغط على التصميم لإظهار نقاط التحكم والملاحظة، لذا يجب تعديلها وتجهيزها لاحقاً.
- الاقتران الوثيق والتبعيات الصلبة. إنشاء متعاونين ملموسين باستخدام
new(أو سحبهم من كائنات فردية ساكنة) يعني أنه لا يمكنك استبدالهم ببدائل اختبار — فيقوم الاختبار بتشغيل الوحدة وكل ما تلمسه. - الحالة العامة/المشتركة والمدخلات المخفية. الكائنات الفردية (singletons)، والمتغيرات الساكنة (statics)، والوقت المحيط، والعشوائية، وقراءات البيئة هي مدخلات لم يصرّح بها الاختبار أبداً ولا يمكنه تعيينها، لذا يكون السلوك ضمنياً وغير قابل للتحكم.
- العمليات غير المتزامنة والتأثيرات الجانبية في النواة. عندما يكون منطق العمل (business logic) متشابكاً مع الخيوط (threads)، أو الطوابير (queues)، أو الإدخال/الإخراج (IO)، أو واجهة المستخدم (UI)، فلا توجد قيمة نقية للتحقق منها.
لماذا يضر ذلك
- الموثوقية. تصبح الاختبارات المجبرة على استخدام الإدخال/الإخراج الحقيقي، أو فترات النوم (sleeps)، أو الكائنات الفردية المشتركة، أو ساعات النظام العامة بطيئة ومتقلبة ومرتبطة بالترتيب — وهو المسار الكلاسيكي للوصول إلى اختبار غير مستقر/هش.
- سهولة الصيانة. تهيئات التجهيز الضخمة والاختبارات التي تصل إلى التفاصيل الداخلية تقرن مجموعة الاختبارات بتفاصيل التنفيذ، لذا فإن عمليات إعادة الهيكلة غير الضارة تؤدي إلى كسر الاختبارات (الاختبار الهش / التجهيز الهش).
- الثقة الزائفة. يتم اختبار أصعب مسارات الكود وأهمها فقط من خلال واجهات خشنة وغير مباشرة — أو يتم تخطيها بالكامل. تبدو أرقام التغطية جيدة بينما يتم تشغيل المنطق البرمجي الأكثر خطورة بصعوبة.
- سهولة القراءة. الاختبار الذي تطغى عليه السقالات (scaffolding) يحجب السلوك الوحيد الذي يفترض به تحديده، وبالتالي لا يوثق شيئاً.
- في الكتالوج المفتوح لروائح الاختبار، يعتبر الكود صعب الاختبار هو النظير في جانب الإنتاج لروائح الاختبار مثل الاختبار غير المباشر (Indirect Testing) و الاختبار الهش (Fragile Test): فالتصميم السيئ هو السبب، والاختبارات المحرجة هي العَرَض.
##Treatment
أصلح التصميم وليس الاختبار. الهدف هو إعطاء كل وحدة نقطة تحكم (طريقة لوضعها في حالة معروفة) و نقطة ملاحظة (طريقة لقراءة النتيجة).
- أدخل شقوقاً عبر حقن التبعية (Dependency Injection). Pass collaborators in (constructor injection) instead of constructing or looking them up. Inject ambient inputs too — clock, id/uuid generator, random source — so they become controllable parameters.
- اعتمد على التجريدات. برمج بالاعتماد على واجهة (interface) واستبدلها بـ stub/fake/mock في الاختبارات. هذا يزيل مباشرة التبعية الصلبة المترابطة لـ Designite ويقلل من التبعية المفرطة.
- طبق نمط الكائن المتواضع (Humble Object). ادفع الأجزاء الصعبة الاختبار حقاً (واجهة المستخدم، غراء العمليات غير المتزامنة، الإدخال/الإخراج المجرد) إلى محول رقيق خالٍ من المنطق، وانقل منطق القرار إلى كائن بسيط متزامن يمكنك اختباره مباشرة.
- استخرج دوالاً نقية (pure functions). افصل الحساب عن التأثيرات الجانبية؛ وتحقق من القيم المرجعة، وقم بإجراء عمليات الإدخال/الإخراج عند الحدود فقط.
- اجعل العمليات غير المتزامنة قابلة للملاحظة. أرجع وعداً (promise) أو اعرض إشارة اكتمال، واحقن المجدول (scheduler) بحيث تستخدم الاختبارات مؤقتات وهمية بدلاً من
sleep. - بالنسبة للكود القديم الذي لا يمكنك إعادة تصميمه بعد، استخدم فئة فرعية مخصصة للاختبار أو شقاً ضيق النطاق (تقنيات فيذرز Feathers) لوضع الكود تحت الاختبار، ثم أعد الهيكلة نحو حقن التبعيات — بدلاً من ترك خطافات
if (testMode)دائمة في كود الإنتاج. - تجنب الحالة العامة/الساكنة. استبدل الكائنات الفردية بتبعيات يتم تمريرها صراحةً حتى لا تتشارك الاختبارات الحالة المخفية أو تعيد ضبطها.
// بعد: التبعيات، الساعة، والمعرف تم حقنهم -> قابل للاختبار بمعزل
class OrderService {
constructor(db, gateway, clock = () => Date.now(), newId = uuid) {
this.db = db; this.gateway = gateway; this.clock = clock; this.newId = newId;
}
placeOrder(cart) {
const id = this.newId();
const charge = this.gateway.charge(cart.total); // تجريد لبوابة الدفع (PaymentGateway)
this.db.save({ id, at: this.clock(), charge });
return id;
}
}
// الاختبار: حتمي، لا توجد شبكة، لا يوجد تعديل عالمي، سريع.
const svc = new OrderService(fakeDb, fakeGateway, () => 1000, () => 'id-1');
expect(svc.placeOrder({ total: 50 })).toBe('id-1');
expect(fakeGateway.charge).toHaveBeenCalledWith(50);
expect(fakeDb.saved[0]).toEqual({ id: 'id-1', at: 1000, charge: fakeGateway.result });
##Detected by
- designite التبعية الصلبة المترابطة (رائحة قابلية الاختبار) — التبعية الصلبة المترابطة (Hard-wired Dependency)
- designite التبعية المفرطة (رائحة قابلية الاختبار) — التبعية المفرطة
- designite الحالة العامة (رائحة قابلية الاختبار) — الحالة العامة
- designite انتهاك قانون ديميتر (رائحة قابلية الاختبار) — انتهاك قانون ديميتر