اختبار App Router في Next.js: ملاحظات ميدانية حول Server Components، محاكاة fetch()، ومتى يتولى Playwright المهمة
جدول المحتويات
عن الكاتب

Ahmed Mahmoud
كاتب ومطوّر
مهندس برمجيات شغوف بتطوير الويب وتصميم تجربة المستخدم.
مؤسّس ديفيا · eng-ahmed.com ↗نقاط أساسية
- لا تستطيع React Testing Library عرض Server Component غير متزامن مباشرة: تعمل RTL عبر react-dom، بينما تُنفَّذ Server Components فقط عبر RSC renderer الذي يحوّل الشجرة إلى تدفق (stream) لا تملك RTL مسارًا للتعامل معه.
- يعمل Vitest عبر آلية Node الافتراضية لحل الوحدات، وليس عبر شرط react-server الذي يستخدمه Next.js لاستبدال وحدات الخادم فقط — استيراد Server Component يستدعي headers() أو cookies() أو حزمة server-only داخل ملف اختبار يؤدي إما لخطأ فوري أو لحلّ الوحدة الخاطئة بصمت.
- الإعداد الذي استقررنا عليه: نفصل جلب البيانات والمنطق التجاري إلى دوال async عادية مُصدَّرة، ونختبرها مباشرة بـ Vitest و Mock Service Worker (msw)، ونترك الـ Server Component نفسه مجرد طبقة توصيل رقيقة.
- الـ Server Action المعرّف بـ 'use server' هو في النهاية دالة async عادية بعد الترجمة — نستدعيها مباشرة داخل اختبار Vitest مع كائن FormData نُنشئه بأنفسنا، دون الحاجة لمتصفح.
- Playwright ليس اختياريًا للمسارات التي تحتوي على loading.tsx أو error.tsx: ترتيب البث وتوقيت واجهات Suspense الاحتياطية لا يظهران إلا عند تنفيذ محرك RSC الحقيقي للمسار كاملاً.
لماذا لا نستطيع عرض Server Component في Next.js باستخدام React Testing Library؟
لأن دالة render() في React Testing Library تستدعي react-dom، بينما لا تُنفَّذ Server Component أبدًا عبر react-dom — بل عبر RSC renderer (المبني على react-server-dom-webpack)، الذي يحوّل شجرة المكوّنات إلى تدفق لا تملك RTL مسارًا لقراءته. كذلك فإن مكوّنًا دالة غير متزامن ينتظر استدعاء قاعدة بيانات أو fetch() ليس شيئًا صُمِّمت render() لانتظاره؛ فهي تتوقع مكوّنًا يُرجع نتيجته بشكل متزامن من أول تمريرة.
يزيد Vitest المشكلة تعقيدًا. يحلّ Next.js وحدات الخادم عبر شرط تصدير خاص باسم react-server يستبدل حزمًا كاملة — next/headers، next/cookies، وأي شيء مُغلَّف بحزمة server-only — بنسخ آمنة للخادم. أما Vitest، الذي يعمل عبر آلية الحلّ الافتراضية في Node، فلا يفعّل هذا الشرط، لذا فإن استيراد Server Component داخل ملف اختبار إما يفشل فورًا على الاستيراد المحمي أو يحلّ بصمت إلى الوحدة الخاطئة. الحالة الثانية هي الأخطر، لأن الاختبار يبقى ناجحًا رغم أن الكود نفسه كان سينهار في الإنتاج.
كيف نختبر فعليًا جلب البيانات داخل Server Component؟
نسحب جلب البيانات والتحويلات خارج المكوّن إلى دوال async عادية مُصدَّرة، ثم نختبر هذه الدوال مباشرة — فهي لا تحتاج إلى RSC renderer بعد فصلها عن JSX. دالة مثل getInvoice(id) تصبح دالة async عادية يمكن لـ Vitest استيرادها وانتظارها كأي وحدة أخرى. أما مكوّن الصفحة نفسه فيبقى رقيقًا لدرجة أننا نتوقف عن اختباره بشكل منفرد؛ تلك التغطية تنتقل إلى Playwright.
هل نستخدم fetch() مباشرة أم نعترضه عبر MSW؟
نعترض الطلبات عبر Mock Service Worker (msw) بدلًا من استبدال دالة fetch نفسها. الاعتراض عند حدود الشبكة يصمد أمام أي إعادة هيكلة تغيّر طريقة استدعاء fetch داخل الدالة، بينما ينكسر vi.fn() بمجرد تغيير ترتيب المعطيات أو تبديل العميل المستخدم. نضبط onUnhandledRequest على 'error' في msw، فيفشل الاختبار فور حدوث أي طلب شبكة حقيقي نسينا محاكاته، بدلًا من أن يصل الاختبار بصمت إلى واجهة حية. بالنسبة لاستدعاء واحد منفرد، يبقى vi.stubGlobal('fetch', vi.fn()) خيارًا كافيًا — نلجأ لـ msw حين تستدعي أكثر من دالة واحدة في الملف fetch.
هل نحتاج متصفحًا لاختبار Server Action؟
لا. تتحوّل دالة 'use server' بعد الترجمة إلى دالة async عادية مُصدَّرة، فيمكن لاختبار Vitest استيرادها واستدعاءها مباشرة بكائن FormData ننشئه بأنفسنا. النقطة التي تُربك الفريق غالبًا هي أن redirect() و revalidatePath() تفشلان عند استدعائهما خارج طلب فعلي — محاكاة next/navigation و next/cache داخل الاختبار تحوّل الدالة المرتبطة بسياق الطلب إلى شيء بسيط بما يكفي ليُنفَّذه Node مباشرة.
متى يحلّ Playwright محل اختبارات الوحدة بالكامل؟
في أي سلوك لا يظهر إلا عند بث محرك RSC الحقيقي للمسار إلى المتصفح: ترتيب واجهات Suspense الاحتياطية من loading.tsx، حدود error.tsx التي تلتقط خطأ مرميًا، عمليات إعادة التوجيه المعتمدة على الكوكيز أو الترويسات، وHTML الفعلي الذي يراه أي زاحف بحث أو محرك إجابات يعتمد على LLM. لا شيء من ذلك يمكن ملاحظته من اختبار وحدة، لأن اختبارات الوحدة لا تُشغّل محرك البث إطلاقًا. نستخدم page.route() في Playwright مع تأخير مصطنع لجعل البث قابلًا للاختبار أصلًا — فبدون ذلك، تُحل المحاكاة السريعة قبل أن تحصل واجهة التحميل حتى على فرصة للظهور.
كيف يبدو هرم اختبار عملي لـ App Router؟
تغطي اختبارات الوحدة بـ Vitest الدوال المستخرجة لجلب البيانات، والتحويلات، وClient Components، ومنطق Server Actions. تغطي طبقة تكامل بـ Vitest و msw سلوك الدالة أمام شبكة واقعية لكن محاكاة. تغطي اختبارات end-to-end بـ Playwright ترتيب البث، وحدود Suspense وerror، وإعادة التوجيه، والمسارات المحمية بالمصادقة، وHTML الفعلي — لا نتخطى هذه الطبقة أبدًا لأي صفحة App Router مهمة، لأنها الطبقة الوحيدة التي تُشغّل المحرك الحقيقي.
الطبقة التي تخلينا عنها تقريبًا بالكامل هي تلك التي تتمحور حولها معظم الدروس التعليمية: تركيب Server Component داخل بيئة شبيهة بـ jsdom والتحقق من مخرجاتها. إنها الطبقة التي جعلتها بنية Next.js نفسها غير موثوقة، فتوقفنا عن مقاومة ذلك ونقلنا تلك التغطية إلى Playwright.
الأسئلة الشائعة
Q: هل يمكننا استخدام React Testing Library لعرض Server Component في Next.js؟
A: ليس مباشرة. تعمل render() في RTL عبر react-dom، بينما لا تعمل Server Component إلا عبر RSC renderer، لذا فإن المكوّنات غير المتزامنة التي تستدعي headers() أو cookies() أو تنتظر قاعدة بيانات ستفشل أو تتجمد تحت RTL. استخرج المنطق إلى دوال عادية أو غطِّ المكوّن بـ Playwright بدلًا من ذلك.
Q: كيف نحاكي fetch() في اختبار Next.js؟
A: نستخدم setupServer من msw/node لاعتراض الطلبات عند طبقة الشبكة، أو vi.stubGlobal('fetch', vi.fn()) لمحاكاة استدعاء واحد داخل اختبار صغير. الاعتراض عند حدود الشبكة يصمد أمام إعادة الهيكلة أفضل من محاكاة الدالة التي تستدعي fetch.
Q: هل نحتاج متصفحًا لاختبار Server Action في Next.js؟
A: لا. الدالة المعرّفة بـ 'use server' هي دالة async عادية مُصدَّرة بعد الترجمة، فنستوردها داخل اختبار Vitest ونستدعيها مباشرة بكائن FormData — ومحاكاة next/navigation وnext/cache تتعامل مع أي استدعاء لـ redirect() أو revalidatePath() بداخلها.
Q: لماذا يفشل Vitest عند استيراد وحدات مخصصة للخادم فقط رغم أنها تعمل بشكل سليم داخل التطبيق؟
A: يحلّ Vitest الوحدات عبر شروط Node الافتراضية، وليس عبر شرط react-server الذي يستخدمه Next.js للفصل بين شجرتي الخادم والعميل، لذا فإن الحزم المحمية بواسطة حزمة server-only قد تفشل أو تُحلّ بشكل خاطئ تحت Vitest العادي. نُبقي منطق الخادم فقط في ملفات نختبرها مباشرة بدلًا من استيرادها عبر Server Component.
Q: هل نستمر بكتابة اختبارات Playwright حتى مع تغطية عالية لاختبارات الوحدة؟
A: نعم، لأي مسار يحتوي على حدود loading.tsx أو error.tsx، أو ترتيب بث، أو إعادة توجيه مرتبطة بالمصادقة — فهذا السلوك لا يظهر إلا عند تشغيل محرك RSC الحقيقي للمسار، ولا يُشغّل أي اختبار وحدة هذا المحرك.
قراءات إضافية
أدوات المطورين
لماذا يصبح فحص أنواع TypeScript بطيئًا: ملاحظاتنا الميدانية حول tsgo ومراجع المشاريع وisolatedDeclarations
عندما يتوقف فحص الأنواع عن كونه جزءًا من دورة التطوير السريعة، يكون حدسنا الأول خاطئًا عادةً: المستودع لم يكبر كثيرًا، بل أصبح مليئًا بالأنواع العامة. نشارك كيف نقيس tsc باستخدام --extendedDiagnostics قبل أي تعديل، وكيف نقيّم منفذ TypeScript الأصلي المكتوب بلغة Go واسمه tsgo، ومتى تستحق مراجع المشاريع وisolatedDeclarations عناء الترحيل.
أدوات المطورين
تشغيل TypeScript مباشرة في Node.js: كيف استغنينا عن خطوة البناء للسكربتات
أصبح Node.js ينفّذ ملفات TypeScript مباشرة عبر إزالة الأنواع. هكذا نقل فريقنا السكربتات والإعدادات والمهام المجدولة إلى التشغيل المباشر، وكيف نُبقي الكود قابلاً للمحو بخيار erasableSyntaxOnly في TypeScript 5.8، ولماذا احتفظت خدمات NestJS لدينا بخطوة البناء.