ConstructiCat Logo
CodeBust.
Browse section ▾

المساواة الحساسة.

يقوم الاختبار بالتحقق من السلوك من خلال مقارنة التمثيل النصي للكائن (toString()، أو JSON.stringify()، أو HTML المصيّر) بسلسلة نصية حرفية متوقعة، مما يربط الاختبار بتنسيقات عرضية بدلاً من القيم المهمة بالفعل.

##Signs and Symptoms

تتعرف على "المساواة الحساسة" عندما يقوم جانب القيمة الفعلية (actual) للتحقق بتحويل كائن إلى نص، ويكون جانب القيمة المتوقعة (expected) عبارة عن سلسلة نصية حرفية تجمع حقولاً متعددة معاً مع الفواصل، وعلامات الاقتباس، والأقواس، والمسافات البيضاء.

العلامات الدالة:

  • تأتي القيمة الفعلية من .toString()، أو String(x)، أو قالب نصي (template literal)، أو JSON.stringify(...)، أو wrapper.html()، أو el.outerHTML، أو لقطة (snapshot) لكائن متسلسل.
  • القيمة المتوقعة عبارة عن سلسلة نصية حرفية طويلة (غالباً ما تُنسخ من تشغيل فاشل سابق) ولا يمكنك معرفة الجزء الذي يخضع للاختبار بمجرد النظر.
  • يتعطل الاختبار عند إجراء تغييرات لا تؤثر على السلوك: إعادة ترتيب المفاتيح، إضافة أو إزالة المسافات البيضاء، دقة الأرقام، تنسيق التاريخ، اللغة المحلية، المنطقة الزمنية، أو ترتيب تكرار عناصر Map/Set/الكائنات.
// مقارنة صيغة متسلسلة بسلسلة نصية حرفية
expect(JSON.stringify(user)).toBe('{"name":"Bob","age":30}');

// التحقق من مخرجات toString()
expect(order.toString()).toBe('Order[id=7, total=42.00, items=2]');

// مقارنة الـ HTML المصيّر بالكامل بسلسلة حرفية
expect(wrapper.html()).toBe('<div class="card"><h2>Bob</h2><span>30</span></div>');

// مقارنة نصوص حساسة للغة المحلية/المنطقة الزمنية
expect(d.toString()).toBe('Mon Jun 15 2026 00:00:00 GMT+0000 (UTC)');

##Reasons for the Problem

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

  • إنها طريقة سريعة وسهلة. كما يلاحظ "فان ديرسن" وآخرون في كتابهم Refactoring Test Code، فإنه من السهل حساب النتيجة الفعلية وتحويلها إلى سلسلة نصية ومقارنتها بسلسلة نصية حرفية تمثل القيمة المتوقعة — وغالباً ما تُنسخ القيمة الحرفية من جولة اختبار فاشلة. وتجعل أدوات اللقطات (Snapshot tooling) هذا الأمر أكثر إغراءً.
  • لا يحتوي الكائن على طريقة مقارنة مساواة ذات مغزى (مثل equals أو مطابقة عميقة deep matcher) مجهزة، أو أن هيكل الكائن (object graph) كبير، لذا فإن تحويله إلى سلسلة نصية يبدو أبسط من التحقق من كل حقل على حدة.

لماذا يضر ذلك

  • الهشاشة / الفشل الزائف. تحمل السلسلة النصية العديد من التفاصيل غير الهامة — مثل الفواصل، وعلامات الاقتباس، والمسافات، وترتيب المفاتيح، ودقة الأرقام العشرية، واللغة المحلية (locale)، والمنطقة الزمنية، وترتيب تكرار عناصر المجموعات. وكلما تغير تنسيق toString() أو تنسيق التسلسل (serialization format)، تبدأ الاختبارات غير المرتبطة بالفشل على الرغم من أن السلوك صحيح. هذا سبب كلاسيكي لـ الاختبار الهش (Fragile Test).
  • قابلية الصيانة. يؤدي تغيير واحد في التنسيق أو في مكتبة برمجية إلى فرض تعديلات عبر العديد من الاختبارات، وتحديث السلاسل الحرفية الضخمة المتوقعة هو أمر ممل وسهل الوقوع فيه في أخطاء دقيقة.
  • المقروئية / غموض الهدف. تحجب القيمة المتوقعة المكتوبة كـ "جدار من النص" ما يتم التحقق منه بالفعل؛ فلا يمكن للمراجع رؤية الحقل الوحيد الذي يهتم به الاختبار.
  • الثقة الزائفة. يمكن أن ينجح تمثيل الكائن النصي (stringified blob) لأسباب خاطئة (مثل حالتين مختلفتين تنتجان نفس النص عند تحويلهما لسلسلة نصية)، كما أنه يدفع المطورين إلى إعادة تسجيل القيمة الحرفية/اللقطة (snapshot) عند الفشل دون التحقق مما إذا كانت القيمة الجديدة صحيحة بالفعل.
  • عدم الاستقرار (Flakiness). تجعل النصوص الحساسة للغة المحلية والمنطقة الزمنية والترتيب النتائج تعتمد على البيئة التي يتم تشغيل الاختبار فيها.

##Treatment

تحقق من البيانات، وليس من طريقة عرضها (rendering).

  1. قارن القيم الهيكلية باستخدام المساواة العميقة (deep equality) بدلاً من مقارنة السلاسل النصية (toEqual/toStrictEqual)، والتي تكون مستقلة عن الترتيب والمسافات البيضاء بالنسبة للكائنات.
  2. تحقق من السمات ذات الصلة فقط باستخدام أدوات المطابقة الجزئية (partial matchers) مثل (toMatchObject، expect.objectContaining) حتى لا يؤدي أي تغيير في التنسيق في مكان آخر إلى إتلاف الاختبار.
  3. حلل البيانات (Parse) ولا تطابق النصوص. عندما تتلقى حمولة متسلسلة (مثل جسم استجابة API بتنسيق JSON)، قم بتحليلها مرة أخرى إلى هيكل وقارن الهياكل.
  4. قم بتقديم طريقة مساواة/مقارنة على الكائن (وهي عملية إعادة الهيكلة التي يوصي بها "فان ديرسن" وآخرون) وتحقق من مساواة الكائنات بدلاً من مساواة toString().
  5. إذا كان يجب عليك مقارنة المخرجات المتسلسلة، فقم بوضعها في صيغة قياسية أولاً (canonicalize) — فرتب المفاتيح، وثبت اللغة المحلية/المنطقة الزمنية، وحدد دقة الأرقام — وتفضيل اللقطة (snapshot) التي تمت مراجعتها على السلسلة الحرفية المضمنة المنسوخة يدوياً.
  6. لاختبارات الـ DOM/المكونات، قم بالاستعلام دلالياً (باستخدام Testing Library مثل getByRole، toHaveTextContent) بدلاً من مقارنة ملفات الـ HTML بالكامل.
// قبل — المساواة الحساسة
expect(JSON.stringify(user)).toBe('{"name":"Bob","age":30}');

// بعد — مساواة هيكلية (مستقلة عن ترتيب المفاتيح والمسافات البيضاء)
expect(user).toEqual({ name: 'Bob', age: 30 });

// بعد — التحقق من الحقل الخاضع للاختبار فقط
expect(user).toMatchObject({ age: 30 });
// قبل — مطابقة السلسلة النصية لحمولة API
expect(res.text).toBe('{"id":7,"total":42}');

// بعد — مقارنة الهياكل المحللة
expect(JSON.parse(res.text)).toEqual({ id: 7, total: 42 });