التفاؤل بالموارد.
التفاؤل بالموارد هو حين يفترض اختبار ما أن مورداً خارجياً (ملف، مجلد، جدول قاعدة بيانات، متغير بيئة، أو نقطة وصول شبكية) موجود بالفعل وفي حالة معروفة دون توفيره والتحقق منه، مما يجعل الاختبار يجتاز أو يفشل بشكل غير حتمي.
##Signs and Symptoms
يلمس اختبار ما شيئاً خارجاً عنه — ملفاً، مساراً مؤقتاً، مجلداً، صفاً في قاعدة بيانات، متغير بيئة، أو نقطة وصول بعيدة — ويثق ببساطة أنه موجود بالفعل ومُشكَّل بشكل صحيح. لا توجد خطوة إعداد تُنشئ المورد ولا فحص لوجوده قبل استخدامه.
العلامات الدالة:
- مسار مُرمَّز بشكل ثابت يُقرأ أو يُكتب دون
existsSync/mkdir/writeFileمسبق: مثلfs.readFileSync('/tmp/app/config.json'). - الاختبار يجتاز على جهازك أو في التشغيل الأول، ثم يفشل في فحص نظيف، أو في CI، أو في مجلد مؤقت مختلف لنظام التشغيل، أو عندما تعمل المجموعة بترتيب مختلف أو بشكل متوازٍ.
- المورد الذي يحتاجه ينشئه فعلياً اختبار آخر (أو خطوة يدوية/خاصة بالتطوير)، فيعمل الاختبار كأثر جانبي لترتيب التشغيل فحسب.
- يفتح ملفاً/اتصالاً ويؤكد على النتيجة دون تأكيد وجود المورد أبداً — مورد مفقود يُطلق استثناءً قبل التأكيد الحقيقي، أو يُعيد بياناً فارغاً بصمت لا يزال «يجتاز».
test('parses the config file', () => {
// متفائل: يفترض أن /tmp/app/config.json موجود مسبقاً وذو صيغة صحيحة
const raw = fs.readFileSync('/tmp/app/config.json', 'utf8');
expect(JSON.parse(raw).port).toBe(8080);
});
الإرشاد الكشفي المعياري (testsmells.org / tsDetect): اختبار يستخدم مورداً شبيهاً بـFile دون استدعاء فحص وجود/صحة أولاً كـexists() أو isFile() أو notExists().
##Reasons for the Problem
لماذا يحدث
- كان المورد موجوداً أثناء كتابة الاختبار (ملف تثبيت في المستودع، أو قاعدة بيانات تطوير مُهيّأة، أو ملف مؤقت تركه خطوة سابقة)، فلم يشعر المؤلف بغيابه.
- الإشارة إلى مسار أو جدول موجود ببساطة أقل كوداً من تخصيص الموارد وتهيئتها في الإعداد وتفكيكها بعد ذلك.
- نسخ ولصق من اختبار آخر يعتمد بالفعل على حالة مشتركة محيطة.
لماذا يضر
- عدم الحتمية / الهشاشة. تعتمد النتيجة على حالة البيئة، لا على الكود قيد الاختبار. يصف van Deursen وآخرون ذلك بالضبط: اختبارات «تعمل بشكل جيد في وقت ما، وتفشل فشلاً ذريعاً في وقت آخر.» بيئات CI النظيفة والفحوصات الحديثة والعمال المتوازيون ومواقع الملفات المؤقتة المختلفة هي البيئات التي تُكشف فيها هذه المشكلة.
- ثقة زائفة. قد يجتاز اختبار أخضر بسبب حالة متبقية من تشغيل سابق أو اختبار سابق، لا لأن الكود الحالي صحيح — أو قد يُطلق مورد مفقود استثناءً مبكراً ولا يُنفَّذ التأكيد المعنوي أبداً.
- اقتران خفي وتبعية للترتيب. حين ينشئ اختبار ما يستهلكه اختبار آخر، يكون للمجموعة عقد ترتيب غير مرئي يتكسر عند الخلط أو التشظية.
- صعوبة إعادة الإنتاج والصيانة. لا يمكن إعادة إنتاج الإخفاقات محلياً لأنها تعتمد على حالة محيطة خاصة بالجهاز، فيغدو التصحيح بطيئاً ويتآكل ثقة الاختبار.
##Treatment
اجعل كل اختبار يمتلك ويتحكم في كل مورد يلمسه، ولا تفترض أبداً حالة محيطة.
- التوفير في الإعداد، التنظيف في التفكيك. استخدم Setup External Resource — خصص وهيّئ الملفات والمجلدات وجداول قواعد البيانات والاتصالات في
beforeEach/beforeAll، وحررها فيafterEach/afterAllحتى يبدأ التشغيل التالي نظيفاً. - أنشئ، لا تفترض. اكتب الملف، أو هيّئ الجدول، أو ابدأ الخادم النائب في الإعداد. إن كان لا بد من استهلاك مورد موجود مسبقاً، تأكد من وجوده أولاً حتى يفشل المورد المفقود بصوت عالٍ برسالة واضحة بدلاً من إفساد التأكيد الحقيقي.
- العزل لكل اختبار. استخدم مجلداً مؤقتاً فريداً (
fs.mkdtemp(os.tmpdir() + …)) أو مخططاً/فضاء أسماء جديداً لكل اختبار بدلاً من مسار ثابت مشترك، حتى لا تتصادم التشغيلات المتوازية وعمليات إعادة التشغيل. - الأفضل من ذلك، أزل التبعية. استبدل المورد الحقيقي بنموذج محاكاة أو بديل في الذاكرة (
fsمُحاكى، قاعدة بيانات في الذاكرة، HTTP مُعترَض) حتى يكون الاختبار مكتفياً بذاته وحتمياً — الحل الذي توصي به testsmells.org.
قبل → بعد:
// قبل — التفاؤل بالموارد
test('parses the config file', () => {
const raw = fs.readFileSync('/tmp/app/config.json', 'utf8');
expect(JSON.parse(raw).port).toBe(8080);
});
// بعد — الاختبار يمتلك موارده ويتحقق منها
import { mkdtemp, writeFile, rm, readFile } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
let dir;
beforeEach(async () => {
dir = await mkdtemp(path.join(os.tmpdir(), 'cfg-'));
await writeFile(path.join(dir, 'config.json'), JSON.stringify({ port: 8080 }));
});
afterEach(() => rm(dir, { recursive: true, force: true }));
test('parses the config file', async () => {
const raw = await readFile(path.join(dir, 'config.json'), 'utf8');
expect(JSON.parse(raw).port).toBe(8080);
});
##Detected by
- tsDetect Resource Optimism — Resource Optimism