---
title: "الاختبار الغامض"
type: "test-smell"
slug: "obscure-test"
url: "http://localhost:3000/ar/test-smells/obscure-test.md"
category: "الروائح الغامضة"
description: "الاختبار الغامض هو اختبار لا يمكن للقارئ أن يحدد من خلال طريقة الاختبار بمفردها ما هو السيناريو المجهز، وما هو السلوك الذي تم تشغيله، وما هي النتيجة المتوقعة — لأن الغرض مدفون في الكثير من التفاصيل، أو قلة السياق، أو منطق مخفي في مكان آخر."
---
# الاختبار الغامض

> الاختبار الغامض هو اختبار لا يمكن للقارئ أن يحدد من خلال طريقة الاختبار بمفردها ما هو السيناريو المجهز، وما هو السلوك الذي تم تشغيله، وما هي النتيجة المتوقعة — لأن الغرض مدفون في الكثير من التفاصيل، أو قلة السياق، أو منطق مخفي في مكان آخر.

## Signs and Symptoms

تقرأ اختباراً ولا تزال لا تستطيع الإجابة على ثلاثة أسئلة: _ما هي التهيئة، وما الذي يتم تشغيله، وما هي النتيجة المتوقعة؟_ سلسلة السبب والنتيجة مخفية. يقسم ميسزاروس الأسباب إلى "معلومات كثيرة جداً" و"معلومات قليلة جداً". العلامات الشائعة:

* **الاختبار المتلهف (Eager Test)** — طريقة اختبار واحدة تتحقق من سلوكيات متعددة غير مترابطة، لذا لا يوجد هدف واحد واضح.
* **الضيف الغامض (Mystery Guest)** — يعيش التجهيز أو القيم المتوقعة خارج الاختبار (ملف، أو سجل قاعدة بيانات مشترك، أو تجهيز يتم تحميله بالاسم)، لذا لا يمكنك معرفة سبب نجاح التحقق.
* **التجهيز العام (General Fixture)** — يقوم خطاف `beforeEach`/مصنع مشترك كبير ببناء ما هو أكثر بكثير مما يحتاجه هذا الاختبار; وتضيع الحقول القليلة ذات الصلة في الضوضاء.
* **المعلومات غير ذات الصلة (Irrelevant Information)** — صفحات من بيانات التهيئة أو لقطة شاشة (snapshot) عملاقة حيث لا يهم سوى حقل أو حقلين فقط.
* **بيانات اختبار ثابتة ومكتوبة يدوياً (Hard-Coded Test Data)** — قيم ثابتة سحرية (مثل `42`، أو `"a3f9-..."`، أو `userId=7`) دون معنى مسمى، وتتكرر عبر التهيئة والتحققات.
* **الاختبار غير المباشر (Indirect Testing)** — يقوم الاختبار بنكز النظام تحت الاختبار من خلال عدة كائنات أخرى، مما يحجب ما هو قيد الاختبار بالفعل.

```js
// غامض: متلهف + ضيف غامض + معلومات غير ذات صلة
test('user', async () => {
  const data = loadFixture('users.json');        // ضيف غامض: القيم تعيش في ملف
  const svc = new UserService(data, cfg, clock); // تجهيز عام: معظم هذا غير مستخدم
  const u = await svc.create({ ...data[0], roles: ['a','b'], flags: 0x1f });

  expect(u.id).toBeDefined();
  expect(u.email).toContain('@');
  expect(await svc.count()).toBe(data.length + 1); // لماذا هذا الرقم؟ الإجابة موجودة في الملف
  expect(await svc.login(u.email, 'p@ss')).toBe(true); // سلوك ثانٍ غير ذي صلة
});

```

هنا لا يمكنك معرفة ما يثبته الاختبار دون فتح ملف `users.json`، وهو يتحقق فعلياً من الإنشاء _و_ تسجيل الدخول في وقت واحد.

## Reasons for the Problem

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

* تنمو الاختبارات بالتراكم: يضيف المطور "تحققاً إضافياً" إلى اختبار موجود بدلاً من كتابة اختبار جديد، مما ينتج عنه اختبار متلهف.
* تبدو مشاركة التهيئة فعالة، لذا يُعاد استخدام تجهيز واحد واسع النطاق أو `beforeEach` في كل مكان (التجهيز العام)، ويتم تحميل ملفات خارجية/بذور قاعدة بيانات بالاسم (الضيف الغامض).
* يؤدي نسخ ولصق بيانات تبدو واقعية إلى سحب العشرات من الحقول غير ذات الصلة والأرقام السحرية.
* المحاكاة المفرطة أو المرور عبر العديد من المتعاونين يحول اختبار الوحدة إلى اختبار غير مباشر (Indirect Testing).

**لماذا يضر ذلك**

* **سهولة القراءة:** الاختبار هو توثيق للسلوك المقصود. إذا لم يتمكن القارئ من إعادة بناء الترتيب ← التشغيل ← التحقق، فإن قيمة هذا التوثيق تضيع.
* **سهولة الصيانة:** عندما يفشل اختبار غامض، فإنك لا تعرف أي سلوك قد تعطل أو ما إذا كان الخطأ في الاختبار أم في الكود، لذا تكون التغييرات بطيئة ومحفوفة بالمخاطر.
* **الموثوقية:** يقرن الضيف الغامض والتجهيز العام الاختبار بالحالة الخارجية/المشتركة، مما يتسبب في إخفاقات متقلبة أو معتمدة على الترتيب (ويكسر خاصية "الجدة والحتمية" لاختبار الوحدة الجيد).
* **الثقة الزائفة:** تحجب الاختبارات المتلهفة التغطية الحقيقية — حيث يؤدي الفشل المبكر في الطريقة إلى إيقاف الفحوصات اللاحقة، لذا فإن السلوكيات التي _تعتقد_ أنها مختبرة قد لا تعمل أبداً. وكما يلاحظ ميسزاروس، فإن أخطاء البرمجة يسهل إخفاؤها في الاختبارات الغامضة، مما ينتج اختبارات معطوبة (Buggy Tests) تنجح لأسباب خاطئة.

## Treatment

اجعل كل اختبار يروي قصة قائمة بذاتها يكون غرضها واضحاً في جسم الاختبار.

1. **تحقق من شرط واحد لكل اختبار.** قسّم الاختبار المتلهف إلى اختبارات مركزة، يحمل كل منها اسماً يوضح السلوك الذي يفحصه. هذا يعالج أيضاً مشكلة حجب التغطية.
2. **أدرج التجهيز ذي الصلة في نفس السطر (Inline) (اقضِ على الضيف الغامض).** ابنِ البيانات التي يعتمد عليها الاختبار _داخل الاختبار نفسه_، أو عبر طريقة إنشاء/بناء صريحة ومسماة — ولا تقم بتحميل ملفات مجهولة أو صفوف مشتركة من قاعدة البيانات.
3. **استخدم تجهيزاً أدنى / جديداً (Minimal / Fresh Fixture).** ابنِ فقط ما يحتاجه هذا الاختبار; واستبدل كتلة `beforeEach` المشتركة الكبيرة ببناء (builder) يضع قيماً افتراضية للضوضاء ويسمح لكل اختبار بتعيين الحقل قيد الاختبار فقط.
4. **سمِّ بياناتك.** استبدل القيم الثابتة السحرية بثوابت/متغيرات توضح النية بحيث يفسر التحقق نفسه.
5. **تحقق من المعنى، وليس من كل شيء.** فضل التحققات المحددة (targeted assertions) على استخدام لقطات الشاشة (snapshots) العملاقة; وإذا استخدمت اللقطات، فاجعلها صغيرة وقابلة للمراجعة حتى لا تغرق الحقائق المهمة في مخرجات غير مهمة.
6. **اخفِ التفاصيل الميكانيكية، وليس الغرض.** ادفع الربط العارض إلى طرق مساعدة/أدوات اختبار مسمية جيداً بحيث يُقرأ جسم الاختبار كتهيئة ← تنفيذ ← توقع.

```js
// قبل (غامض)
test('user', async () => {
  const data = loadFixture('users.json');
  const svc = new UserService(data, cfg, clock);
  const u = await svc.create({ ...data[0], roles: ['a','b'], flags: 0x1f });
  expect(await svc.count()).toBe(data.length + 1);
});

// بعد: غرض واحد، تجهيز مضمن، بيانات مسماة
test('create() persists a new user', async () => {
  const svc = userServiceWith([]);                 // تجهيز أدنى وجديد
  const newUser = aUser({ email: 'ada@example.com' }); // يضع البناء (builder) قيماً افتراضية للضوضاء

  const created = await svc.create(newUser);

  expect(created.email).toBe('ada@example.com');
  expect(await svc.count()).toBe(1);               // يفسر نفسه بنفسه، لا يوجد ملف خارجي
});

```

احرص على اتباع هيكل AAA (الترتيب والتنفيذ والتحقق): يجب أن يستوعب القارئ السيناريو دون مغادرة طريقة الاختبار.

## Detected by

- **eslint-plugin-jest** `max-expects` — jest/max-expects (https://github.com/jest-community/eslint-plugin-jest/blob/main/docs/rules/max-expects.md)
- **eslint-plugin-vitest** `max-expects` — vitest/max-expects (https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/max-expects.md)
- **eslint-plugin-jest** `no-large-snapshots` — jest/no-large-snapshots (https://github.com/jest-community/eslint-plugin-jest/blob/main/docs/rules/no-large-snapshots.md)
- **eslint-plugin-vitest** `no-large-snapshots` — vitest/no-large-snapshots (https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/no-large-snapshots.md)
- **eslint-plugin-jest** `max-nested-describe` — jest/max-nested-describe (https://github.com/jest-community/eslint-plugin-jest/blob/main/docs/rules/max-nested-describe.md)
