ConstructiCat Logo
CodeBust.
Browse section ▾

للمختبرين فقط.

يحتوي كود الإنتاج على طرق، أو وصولات للحالة (accessors)، أو شقوق (seams) توجد فقط لتستخدمها الاختبارات، مما يلوث واجهة برمجة التطبيقات (API) الحقيقية ويدعو إلى اختبارات تتحقق من التفاصيل الداخلية بدلاً من السلوك.

##Signs and Symptoms

تجد أعضاءً في كود الإنتاج لا توجد لهم أي استدعاءات إلا في ملفات الاختبار. العلامات الدالة:

  • الطرق، أو الحصول/التعيين (getters/setters)، أو التصديرات (exports)، أو وسائط الباني المستخدمة حصرياً من ملفات *.test.ts / *.spec.ts.
  • الأسماء أو العلامات ذات النكهة المخصصة للاختبار: مثل getStateForTest، أو resetForTesting، أو __getInternal، أو FTO_*، أو forTest، أو تعليقات مثل // only used by tests، أو تعليقات توضيحية (annotations) مثل @VisibleForTesting / @TestOnly / @internal.
  • تسهيل شروط الرؤية (مثل جعل الحقل public/مصدراً، أو تحويل #private إلى protected) لمجرد تمكين الاختبار من الوصول للحالة الداخلية.
  • إضافة شقوق (seams) إضافية — مثل setClock(...)، أو setRandom(...)، أو reset() — فقط لأن اختباراً احتاج إليها، وليس لأن التصميم الحقيقي يتطلبها.
// payment-service.ts  (كود الإنتاج)
export class PaymentService {
  #ledger: Entry[] = [];

  charge(amount: number) { /* ... */ }

  // لا شيء في الإنتاج يستدعي هذه أبداً — الاختبارات فقط تفعل ذلك:
  getLedgerForTest() { return this.#ledger; }          // يكشف التفاصيل الداخلية
  setClockForTest(now: () => Date) { this.now = now; } // شق للاختبار فقط
  FTO_reset() { this.#ledger = []; }                   // "للاختبارات فقط"
}

فحص سريع: grep -rn 'forTest\|ForTesting\|FTO_' src/ يرجع نتائج في كود الإنتاج، أو استخدام فحص الكود الميت (مثل Knip في وضع الإنتاج) يبلغ بأن تصديراً ما غير مستخدم بينما يستورده اختبار ما بوضوح.

##Reasons for the Problem

لماذا يحدث ذلك

  • ملاءمة الاختبارات لكود غير قابل للاختبار. عندما لا يكون الكود القديم (legacy code) مصمماً لقابلية الاختبار، فإن أسرع طريقة للتحقق من النتيجة هي إحداث ثغرة في النظام تحت الاختبار (SUT) وقراءة تفاصيله الداخلية — ويدرج ميسزاروس هذا كسبب رئيسي لرائحة للاختبارات فقط.
  • واجهات برمجة تطبيقات غير متماثلة (Asymmetric APIs). يستخدم العملاء الحقيقيون الكائن بطريقة واحدة (الكتابة)؛ بينما تستخدمه الاختبارات بشكل متماثل (الكتابة ثم القراءة للتحقق)، لذا "يحتاج" المختبرون إلى وصولات (accessors) لا يحتاجها أي مستدعٍ في كود الإنتاج.
  • ضغط الوقت. إضافة باب خلفي يبدو أرخص في الوقت الحالي من إعادة هيكلة الكود (refactoring) للوصول إلى تصميم يمكن فيه ملاحظة السلوك من خلال العقد العام.

لماذا يضر ذلك

  • سهولة القراءة. لم تعد واجهة برمجة التطبيقات العامة تظهر الحقيقة — فلا يمكن للمسؤولين عن الصيانة تمييز العقد الحقيقي عن سقالات الاختبار (scaffolding)، ويتعين على كل قارئ التساؤل "هل هذه الطريقة مستخدمة بالفعل؟"
  • الكبسلة والموثوقية (Encapsulation & reliability). تصبح الحالة الداخلية قابلة للوصول والتعديل في الكود المشحون. استدعاء شارد لـ FTO_reset() أو تعيين مسرب (leaked setter) يمكن أن يفسد الحالة في الإنتاج؛ كما أن المساحة الإضافية تضخم حزمة الكود وتوسع مساحة الهجوم.
  • الثقة الزائفة. الاختبارات التي تبحث في الحالة الخاصة تتحقق من التنفيذ وليس السلوك. يمكن أن تظل باللون الأخضر بينما العقد العام معطل، وتتحطم عند إجراء عمليات إعادة هيكلة غير ضارة — وهي اختبارات هشّة تختبر الشيء الخاطئ.
  • سهولة الصيانة. تبدو الأعضاء المخصصة للاختبار فقط ككود ميت ولكن لا يمكن حذفها؛ ويجب أن يراعي كل تغيير وجود مستدعين وهميين، وتميل هذه الرائحة إلى التضاعف مع زيادة الاختبارات التي تعيد استخدام الباب الخلفي.

##Treatment

تعامل مع الباب الخلفي كإشارة تصميمية، وليس كتفصيلة تجهيز.

  1. اختبر من خلال السلوك الملاحظ أولاً. تحقق من القيم المرجعة، أو الأحداث المنبعثة، أو المخرجات المحفوظة، أو التفاعلات مع المتعاونين (عبر بدائل الاختبار) بدلاً من الوصول للتفاصيل الداخلية. تختفي معظم وصولات getXForTest بمجرد التحقق مما يفعله الكائن، وليس ما يحتفظ به.
  2. استخدم فئة فرعية مخصصة للاختبار (Test-Specific Subclass) عندما تحتاج فعلياً للوصول الداخلي. قم بتوسيع الفئة في ملف الاختبار لعرض عضو protected، بدلاً من توسيع نطاق الرؤية في كود الإنتاج.
  3. اجعل الشقوق جزءاً من التصميم الحقيقي، وليس فتحات للاختبار فقط. حقن الساعة أو مولد الأرقام العشوائية عبر الباني العادي هو حقن تبعية شرعي؛ أما معدل setClockForTest() فهو رائحة. إذا كان الشق منطقياً للاختبارات فقط، فادفع السلوك إلى نمط الاستراتيجية/الكائن الفارغ (Strategy/Null Object) الذي يثبته كود الإنتاج افتراضياً وتقوم الاختبارات باستبداله.
  4. إذا كان الكشف أمراً لا مفر منه حقاً، فميزه بوضوح وعزله. وضع علامة @VisibleForTesting / @internal (أو اصطلاح تسمية FTO_) وافرض عدم قيام كود الإنتاج باستدعائه مطلقاً (انظر كواشف الرائحة). الشق المميز والمحمي أفضل من الشق الصامت.
  5. تتبع المخالفات الحالية. استخدم grep للبحث عن forTest/ForTesting/FTO_، وقم بتشغيل أداة كشف الاستخدام/الكود الميت مثل Knip في وضع الإنتاج للكشف عن التصديرات المشار إليها من ملفات الاختبار فقط.
// قبل — يحتوي كود الإنتاج على وصول مخصص للاختبار فقط
export class Cart {
  #items: Item[] = [];
  add(i: Item) { this.#items.push(i); }
  getItemsForTest() { return this.#items; } // للمختبرين فقط
}
// اختبار
expect(cart.getItemsForTest()).toHaveLength(1);
// بعد — التحقق من السلوك عبر العقد الحقيقي
export class Cart {
  #items: Item[] = [];
  add(i: Item) { this.#items.push(i); }
  get count() { return this.#items.length; }
  get total() { return this.#items.reduce((s, i) => s + i.price, 0); }
}
// اختبار
cart.add({ price: 10 });
expect(cart.count).toBe(1);
expect(cart.total).toBe(10);

إذا كنت لا تزال بحاجة إلى وصول داخلي، فاحصر الشق في الاختبارات باستخدام فئة فرعية بدلاً من جعلها متاحة للجميع:

// يظل كود الإنتاج نظيفاً: الطابور (queue) محمي وليس عاماً
export class Scheduler {
  protected queue: Job[] = [];
  enqueue(j: Job) { this.queue.push(j); }
}
// ملف الاختبار فقط
class TestScheduler extends Scheduler {
  peek() { return this.queue; }
}

##Detected by

  • sonar java:S5803يجب عدم الوصول إلى الأعضاء المميزين بـ "@VisibleForTesting" من كود الإنتاج