شعار ConstructiCat
CodeBust.
تصفح القسم ▾

اختبار تفاصيل التنفيذ.

يقوم الاختبار بالتحقق من كيفية عمل الكود داخلياً — مثل الحقول الخاصة، أو استدعاءات الدوال الداخلية، أو بنية DOM، أو فئات CSS — بدلاً من التحقق من السلوك الملاحظ الذي يعتمد عليه المستخدم الفعلي.

##العلامات والأعراض

تتعرف على هذه المشكلة عندما يتجاوز الاختبار العقد العام (public contract) ويقوم بتثبيت الآلية الداخلية التي تقف خلفه. العلامات الشائعة:

  • التحقق من الحالة الخاصة/الداخلية (private/internal state) أو الحقول، أو استدعاء الدوال الخاصة مباشرة (غالباً عبر التحويلات القسرية للأنواع (casts)، أو الانعكاس (reflection)، أو استخدام // @ts-expect-error).
  • التجسس (Spying) أو التحقق من استدعاء دالة مساعدة داخلية (مثل expect(internalCalc).toHaveBeenCalled()) بدلاً من التحقق من النتيجة.
  • اختبارات واجهة المستخدم التي تستعلم باستخدام فئة الـ CSS، أو الوسم (tag)، أو خطافات data-*، أو موضع DOM بدلاً من الاستعلام باستخدام الدور (role) أو التسمية (label) أو النص — على سبيل المثال: container.querySelector('.btn-primary > span:nth-child(2)')، أو wrapper.state()، أو wrapper.find('SomeChildComponent').props().
  • لقطات (snapshots) لـكائنات داخلية متسلسلة — نسخ المكوّنات، والأجزاء الداخلية للمخزن (store)، وهياكل البيانات الخاصة — بدلًا مما يراه المستخدم. (أما اللقطة الضخمة لكامل المخرجات المصيَّرة فمشكلة أخرى: الاختبار المحدد بشكل مفرط.)
  • الاختبارات التي تتعطل مع كل عملية إعادة هيكلة على الرغم من أن الميزة لا تزال تعمل بشكل صحيح (السلبيات الزائفة)، وبالعكس تظل ناجحة بعد وجود خلل منطقي لأنها تتحقق فقط من التوصيلات (الإيجابيات الزائفة).
// مشكلة (SMELL): يربط الاختبار بالتفاصيل الداخلية للمكون وبنية الـ DOM
test('counter increments', () => {
  const wrapper = mount(<Counter />);
  wrapper.instance().handleClick();          // يستدعي دالة خاصة مباشرة
  expect(wrapper.state('count')).toBe(1);    // يتحقق من الحالة الداخلية
  expect(wrapper.find('.count-display').text()).toBe('1'); // محدد CSS هِش
});

يظهر نفس الشكل في جانب الخادم (server-side): مثل expect(service._cache.size).toBe(1) الذي يتحقق من ذاكرة تخزين مؤقت خاصة بدلًا مما يحصل عليه الكود المستدعي.

روائح مجاورة: التحقق من أكثر مما يحتاجه السلوك — العدد والترتيب الدقيقان للاستدعاءات، والمطابِقات المفرطة الدقة، واللقطات الضخمة — هو الاختبار المحدد بشكل مفرط؛ والاختبار الذي يستبدل بكل متعاون كائن محاكاة هو المحاكاة المفرطة. أما هذه المشكلة فتتعلق بالموضع الذي ينظر إليه التحقق: الأجزاء الداخلية مثل بنية DOM أو الحالة الخاصة، بدلًا مما يستطيع المستخدم أو الكود المستدعي ملاحظته.

##أسباب المشكلة

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

  • الوصول إلى الحالة الداخلية سهل للغاية — فالحقول العامة، أو الدوال المساعدة المصدرة، أو container.querySelector تكون متاحة مباشرة، في حين أن تشغيل السلوك الفعلي يتطلب إعداداً أكثر تعقيداً.
  • السعي وراء مقاييس التغطية (coverage metrics): فيبدو اختبار كل دالة خاصة بنسبة 1:1 أمراً شاملاً.
  • يدفع الاستخدام المكثف للمحاكاة الافتراضية (mocking) المطورين للتحقق مما إذا كان "قد تم استدعاء هذا المساعد" بدلاً من التحقق مما إذا كان "قد حدث الشيء الصحيح".
  • الأدوات التي تشجع على ذلك: مثل واجهات برمجة التطبيقات للتصيير السطحي (shallow rendering) أو instance() أو state()، أو جلب عقد DOM باستخدام فئة الـ CSS (class name).

لماذا يضر ذلك

  • قابلية الصيانة / الهشاشة. هذا ما يسميه "ميسزاروس" بالـ الاختبار الهش (Fragile Test) الناتج عن البرمجيات المحددة بشكل مفرط (Overspecified Software): حيث يقوم الاختبار بتثبيت سلوك لم يطلبه المستخدم أبداً، وبالتالي فإن عمليات إعادة الهيكلة غير الضارة (إعادة تسمية دالة، أو إعادة هيكلة المارك-أب، أو تغيير حقل خاص) تؤدي إلى تعطل الاختبارات الناجحة دون سبب حقيقي. وتصبح الاختبارات عبئاً على عملية إعادة الهيكلة بدلاً من أن تكون شبكة أمان لها.
  • الثقة الزائفة (الخطر الأساسي، وفقاً لـ Kent C. Dodds). تفشل اختبارات تفاصيل التنفيذ في كلا الاتجاهين الخاطئين: السلبيات الزائفة (false negatives) (يفشل الاختبار على الرغم من أن الميزة لا تزال تعمل بشكل صحيح) والإيجابيات الزائفة (false positives) (ينجح الاختبار على الرغم من تعطل الميزة — لأنك تحققت من التوصيلات وليس من النتيجة). وفي كلتا الحالتين، تتوقف مجموعة الاختبارات عن إخبارك بالحقيقة.
  • المقروئية. يوثق الاختبار كيفية بناء الكود، وليس ما يضمنه. لا يمكن للقارئ معرفة أي السلوكيات تهم بالفعل، ولا يعود الاختبار صالحاً ليكون مثالاً للاستخدام أو مواصفة للكود (spec).
  • الاقتران (Coupling). يثبت الاختبار قرارات التصميم الحالية، مما يثبط عمليات إعادة الهيكلة التي كان من المفترض أن تجعلها الاختبارات آمنة.

##المعالجة

قم بالاختبار من خلال العقد العام (public contract) — وهو نفس السطح الذي يلمسه المتصل الفعلي أو المستخدم — وتحقق من المخرجات الملاحظة: قيم الإرجاع، أو الأخطاء التي تم إلقاؤها، أو الأحداث المنبعثة، أو الحالة المحفوظة، أو واجهة المستخدم المصيّرة/المرئية.

  1. حدد المستهلك (consumer). بالنسبة للوحدة البرمجية (module)، فإن المستهلك هو واجهة برمجة التطبيقات المصدرة (exported API) الخاصة بها؛ وبالنسبة لمكون واجهة المستخدم، فإنه المستخدم (النقرات، الكتابة) وما يمكنه رؤيته.
  2. قم بتقديم المدخلات بنفس الطريقة التي يقوم بها المستهلك، وليس عن طريق استدعاء دوال خاصة. قم بتوليد نقرة حقيقية بدلاً من استدعاء معالج الحدث (handler)؛ واستدعِ الدالة العامة بدلاً من الدالة المساعدة الداخلية.
  3. تحقق من النتائج وليس من التفاصيل الداخلية. استبدل فحوصات state() أو الحقول الخاصة أو toHaveBeenCalled بالتحقق مما ينتج عن العملية.
  4. استعلم عن عناصر واجهة المستخدم من خلال إمكانية الوصول (accessibility) وليس البنية — مثل الدور (role)، والتسمية (label)، والنص — بدلاً من فئات CSS، أو الوسوم، أو nth-child.
  5. توقف عن اختبار الدوال الخاصة مباشرة. قم بتغطيتها من خلال اختبار الدالة العامة التي تستخدمها؛ وإذا كانت الوحدة الخاصة معقدة لدرجة تتطلب اختبارات منفردة، فهذا مؤشر على الحاجة إلى استخلاصها في وحدة برمجية منفصلة تمتلك واجهة برمجة تطبيقات عامة خاصة بها.
  6. احصر استخدام التحقق من المحاكاة/التجسس (mock/spy) على الحدود الحقيقية للبرنامج (الشبكة، الوقت، بوابة الدفع) حيث يكون الاستدعاء نفسه هو السلوك الملاحظ — وليس للمساعدين الداخليين. انظر أيضًا المحاكاة المفرطة.
// قبل: يختبر تفاصيل التنفيذ
const wrapper = mount(<Counter />);
wrapper.instance().handleClick();
expect(wrapper.state('count')).toBe(1);
expect(wrapper.find('.count-display').text()).toBe('1');

// بعد: يختبر السلوك الملاحظ عبر العقد العام الموجه للمستخدم
render(<Counter />);
await userEvent.click(screen.getByRole('button', { name: /increment/i }));
expect(screen.getByText('1')).toBeInTheDocument();

قاعدة عامة: إذا أدت عملية إعادة هيكلة تحافظ على السلوك إلى تعطل الاختبار، فهذا يعني أن الاختبار كان يتحقق من تفاصيل التنفيذ.

##أدوات الكشف

  • eslint-testing-library testing-library/no-node-access — يمنع الوصول المباشر إلى عُقد DOM (querySelector وparentElement وchildren…) بدلًا من الاستعلامات الموجّهة للمستخدم
  • eslint-testing-library testing-library/no-container — يمنع الاستعلام عن العناصر عبر container الذي تعيده render