Node.js / المتوسط: حدود التطبيق / الدرس 9
اختبار السلوك بالأدوات المدمجة
اختر تحققات مفيدة واعزل الآثار الجانبية واختبر فشل العمليات غير المتزامنة دون خدمات خارجية.
في هذه الصفحة
أهداف الدرس
- اشرح المفاهيم والمفاضلات المتعلقة بموضوع «اختبار السلوك بالأدوات المدمجة».
- توقع المثالين وتحقق من مخرجاتهما وسلوك تنظيف الموارد فيهما.
- شخّص الإخفاقات الثلاثة الشائعة وطبق الحلول المشروحة على مدخلات مشابهة.
اختبار سلوك ظاهر
يحدد الاختبار المفيد ما ينبغي حدوثه لمدخل ذي معنى بدلا من تكرار التنفيذ سطرا بسطر. ابدأ بقاعدة عمل نقية وغط الحالات العادية والحدود والمدخلات المرفوضة، ثم أضف اختبارات التكامل عند التقاء المكونات. يفحص التحقق عقدا ظاهرا. قد يفشل اختبار يستوثق فقط من استدعاء مساعد خاص بعد إعادة تنظيم سليمة، مع إغفال نتيجة خاطئة. سم الاختبارات بالسلوك الذي تحميه حتى يوضح الفشل المتطلب المخالف.
استخدام التحققات بدقة
توفر node:assert/strict أدوات equal وdeepEqual وthrows وrejects لعقود مختلفة. قارن البيانات المركبة بنيويا، واختبر أسماء الأخطاء أو رموزها المستقرة بدلا من مكدس كامل. يفحص المثال الأول سلة فارغة ومجموعا حقيقيا وعددا غير صالح، ولا يطبع الملخص إلا بعد نجاح التحققات. هذا عرض بسيط؛ ويستخدم المشروع عادة node:test لتجميع الحالات والإبلاغ بحالة العملية. احسب القيم المتوقعة مستقلا كي يستطيع الاختبار كشف خطأ التنفيذ.
انتظار العملية المختبرة
يجب أن يعيد الاختبار غير المتزامن الوعد المهم أو ينتظره، وإلا فقد ينتهي قبل التحقق أو الفشل. تتوقع assert.rejects رفضا غير متزامن، وتغطي assert.throws الرمي المتزامن. يفحص المثال الثاني وعدا ناجحا ورفضا من نوع معلوم. لا تستبدل await بانتظار زمني عشوائي؛ فهو يبطئ الاختبار ولا يثبت اكتمال العمل الصحيح. استخدم حدثا صريحا أو وعدا مُرجعا أو اعتمادية متحكما بها، وضع مهلة للعمل المحتمل تعليقه.
import { run } from 'node:test';
import { mkdtemp, writeFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
const dir = await mkdtemp(join(tmpdir(), 'test-'));
try {
const file = join(dir, 'cart.test.mjs');
await writeFile(file, `
import test from 'node:test';
import assert from 'node:assert/strict';
function total(lines) {
return lines.reduce((sum, line) => {
if (!Number.isInteger(line.count) || line.count < 0) throw new RangeError('Invalid count');
return sum + line.price * line.count;
}, 0);
}
test('empty cart', () => assert.equal(total([]), 0));
test('valid total', () => assert.equal(total([{ price: 3, count: 2 }, { price: 4, count: 1 }]), 10));
test('negative count', () => assert.throws(() => total([{ price: 3, count: -1 }]), RangeError));
`);
let passed = 0;
let failed = 0;
for await (const event of run({ files: [file] })) {
if (event.type === 'test:pass') passed++;
if (event.type === 'test:fail') failed++;
}
if (failed || passed !== 3) throw new Error('Test suite failed');
console.log(`${passed} tests passed`);
} finally { await rm(dir, { recursive: true, force: true }); }
3 tests passed
ينشئ البرنامج ملف اختبار معزولا ويستخدم node:test لتشغيل ثلاث حالات سلوكية مسماة. يراقب أحداث المشغّل ولا يطبع الملخص إلا بعد نجاحها كلها، ثم يحذف الملفات المؤقتة داخل finally.
امتلاك بيانات الاختبار والتنظيف
تتيح المجلدات المؤقتة والمنافذ المؤقتة تشغيل الاختبارات دون التنافس على ملفات مشتركة أو منفذ ثابت. أنشئ الموارد داخل الاختبار وسجل تنظيفها فورا باستخدام finally أو خطاف تنظيف. لا تستخدم اعتمادات أو بيانات إنتاج. احقن ساعة أو مستودعا أو عميلا خارجيا عندما تحتاج إلى التحكم بسلوكه، مع الإبقاء على تكامل للحد الحقيقي. قد تختبر كثرة البدائل نظاما خياليا فقط. يكشف طلب HTTP حقيقي صغير إلى خادم محلي مغلق ذاتيا أخطاء توصيل لا يكشفها كائن استجابة بديل.
import assert from 'node:assert/strict';
async function lookup(id) {
if (id !== 1) { const error = new Error('Missing'); error.code = 'NOT_FOUND'; throw error; }
return { id: 1, name: 'Ada' };
}
assert.deepEqual(await lookup(1), { id: 1, name: 'Ada' });
await assert.rejects(() => lookup(2), { code: 'NOT_FOUND' });
console.log('async success and failure passed');
async success and failure passed
ينتظر الاختبار تحقق الرفض فلا يعلن الاكتمال قبل فحص مسار الفشل.
تشغيل مجموعة اختبارات مفيدة
احفظ الاختبار بلاحقة .test.mjs واستورد test من node:test وسجل حالات مسماة تحتوي التحققات نفسها. شغّل node --test لاكتشافها وتنفيذها بالإصدار المثبت. يتضمن التقرير زمنا متغيرا، لذا تحقق من السلوك لا من نص التقرير. تبين التغطية شيفرة لم تُنفذ لكنها لا تثبت جودة التحققات. أضف اختبارات رجوع للأخطاء مع إعادة إنتاج محددة، واجعل الاختبارات ثابتة بما يكفي ليستحق فشلها التحقيق بدلا من إعادة تشغيل آلية.
الأخطاء الشائعة وإصلاحها
ينجح الاختبار قبل حدوث الفشل
- لماذا يحدث؟
- بدأ الاختبار عملا غير متزامن دون انتظاره.
- كيف تصلحه؟
- أعد العملية أو انتظرها مع تحققاتها.
- كيف تتجنب تكراره؟
- راجع الاختبارات غير المتزامنة بحثا عن وعود منفصلة.
تفشل الاختبارات عند جمعها فقط
- لماذا يحدث؟
- تشترك في حالة أو ملفات أو منافذ أو تغييرات بيئة.
- كيف تصلحه؟
- أنشئ موارد معزولة واستعد أي تغيير مقصود.
- كيف تتجنب تكراره؟
- حدد ملكية الموارد قبل تشغيل الاختبارات بالتزامن.
التغطية عالية وتفلت الأخطاء
- لماذا يحدث؟
- لا تميز التحققات السلوك الصحيح من النتائج الخاطئة.
- كيف تصلحه؟
- تحقق من نتائج محددة وحالات حدودية.
- كيف تتجنب تكراره؟
- اسأل هل يفشل الاختبار عند رجوع خطأ محتمل.
اختبر فهمك
1. اختر تحققا لدالة تعيد وعدا مرفوضا بالرمز DENIED.
اعرض الحل المشروح
استخدم await assert.rejects(() => operation(), { code: "DENIED" }). تفحص assert.throws الرمي المتزامن فقط. يضمن الانتظار ملاحظة الرفض ويجعل غيابه يفشل الاختبار بدلا من ضياعه بعد الاكتمال.
2. يفتح اختبار خادما محليا ثم يفشل تحقق. كيف تتجنب المجموعة التعليق؟
اعرض الحل المشروح
ابدأ الخادم بالمنفذ صفر والتقط عنوانه الفعلي وسجل إغلاقه في finally أو خطاف التنظيف قبل التحقق. يجب تنفيذ التنظيف عند الفشل والنجاح. أضف مهلة محدودة للعميل كي يُبلغ عن الاستجابة غير المكتملة بصورة متوقعة.
أهم ما تعلمته
- يحدد الاختبار المفيد ما ينبغي حدوثه لمدخل ذي معنى بدلا من تكرار التنفيذ سطرا بسطر.
- توفر node:assert/strict أدوات equal وdeepEqual وthrows وrejects لعقود مختلفة.
- احفظ الاختبار بلاحقة .test.mjs واستورد test من node:test وسجل حالات مسماة تحتوي التحققات نفسها.