NestJS — Node.js للمؤسسات

الاختبار الشامل من البداية إلى النهاية (E2E)

16 دقيقة الدرس 44 من 48

الاختبار الشامل من البداية إلى النهاية (E2E)

يُشغّل الاختبار الشامل (E2E) تطبيق NestJS بالكامل داخل بيئة الاختبار، ويُرسل طلبات HTTP حقيقية عبره، للتحقّق من أنّ كل طبقة — التوجيه، والحُرّاس، والأنابيب، والخدمات، وقاعدة البيانات — تعمل معًا بشكل صحيح. على خلاف اختبارات الوحدة التي تعزل فئةً واحدة، تمنحك اختبارات E2E ثقةً بأنّ التطبيق المترابط يتصرّف بشكل صحيح من منظور العالم الخارجي.

الأدوات: Supertest + Jest

يوفّر سقالة NestJS مجلد test/ مع إعداد E2E جاهز. القطعتان الأساسيتان هما:

  • Supertest — مكتبة تأكيد HTTP سلسة. تقبل خادم HTTP لـ Express (أو Fastify) وتتيح إجراء طلبات والتأكيد على رموز الحالة والترويسات والأجسام دون الارتباط بمنفذ حقيقي.
  • Jest — منفّذ الاختبار. يتضمّن NestJS إعدادًا منفصلًا jest-e2e في package.json يضبط rootDir على test/ وtestRegex على .e2e-spec.ts.
شغّل اختبارات E2E بـ npm run test:e2e. يستدعي هذا jest --config ./test/jest-e2e.json، وهو منفصل عن تشغيل اختبارات الوحدة (npm test). أبقِ الحزمتين منفصلتين حتى لا تُبطّئ إعدادات قاعدة البيانات اختبارات الوحدة السريعة.

تشغيل التطبيق الكامل في وحدة اختبار

استخدم Test.createTestingModule() — نفس واجهة برمجة اختبارات الوحدة — لكن استورد AppModule الحقيقية بدلًا من شريحة صغيرة. استدعاء .compile() ثم app.init() يُشغّل دورة حياة NestJS (تهيئة الوحدة، خطّافات onApplicationBootstrap، وما إلى ذلك):

import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication, ValidationPipe } from '@nestjs/common'; import * as request from 'supertest'; import { AppModule } from '../src/app.module'; describe('Auth (e2e)', () => { let app: INestApplication; beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleFixture.createNestApplication(); app.useGlobalPipes(new ValidationPipe({ whitelist: true })); await app.init(); }); afterAll(async () => { await app.close(); }); });
طابق إعداد main.ts الخاص بك. إذا كان main.ts يُسجّل ValidationPipe أو CORS أو بادئة عالمية، فطبّق نفس الإعداد داخل beforeAll. نسيان هذا يجعل الاختبارات تنجح محليًا لكنّها تفشل مقابل التطبيق الحقيقي.

إعداد قاعدة بيانات الاختبار وتنظيفها

إجراء اختبارات E2E على قاعدة البيانات الإنتاجية أو التطويرية أمر خطير. النهج القياسي هو توجيه تشغيل الاختبار إلى قاعدة بيانات اختبار مخصّصة عبر متغيّرات البيئة، ثم التنظيف بعد كل اختبار أو مجموعة:

  • اضبط NODE_ENV=test وأنشئ ملف .env.test (أو استخدم إعداد TypeORM منفصل) يستهدف قاعدة بيانات اختبار فقط أو قاعدة بيانات SQLite في الذاكرة.
  • شغّل الترحيلات قبل بدء المجموعة (أو استخدم synchronize: true للاختبارات فقط).
  • اقتطع أو أعِد تراجع البيانات بين الاختبارات لتجنّب الإخفاقات المعتمدة على الترتيب.
// في beforeAll — شغّل الترحيلات لضمان نظافة المخطط import { DataSource } from 'typeorm'; beforeAll(async () => { // ... أنشئ التطبيق وهيّئه كما هو مبيّن أعلاه ... const dataSource = app.get(DataSource); await dataSource.runMigrations(); }); afterAll(async () => { const dataSource = app.get(DataSource); await dataSource.dropDatabase(); // امسح قاعدة بيانات الاختبار await dataSource.destroy(); await app.close(); });

اختبار نقاط النهاية المحميّة بالمصادقة من البداية إلى النهاية

يتضمّن تدفّق مصادقة E2E الكامل عادةً مرحلتين: الحصول على رمز، ثم استخدامه. تجعل سلاسل Supertest هذا مقروءًا:

describe('POST /auth/login', () => { it('returns a JWT for valid credentials', async () => { const res = await request(app.getHttpServer()) .post('/auth/login') .send({ email: 'alice@example.com', password: 'secret' }) .expect(201); expect(res.body.access_token).toBeDefined(); }); }); describe('GET /users/me (protected)', () => { let token: string; beforeAll(async () => { const res = await request(app.getHttpServer()) .post('/auth/login') .send({ email: 'alice@example.com', password: 'secret' }); token = res.body.access_token; }); it('returns the authenticated user', () => { return request(app.getHttpServer()) .get('/users/me') .set('Authorization', `Bearer ${token}`) .expect(200) .expect((res) => { expect(res.body.email).toBe('alice@example.com'); }); }); it('rejects unauthenticated requests with 401', () => { return request(app.getHttpServer()) .get('/users/me') .expect(401); }); });

ما يجب التأكيد عليه

  • رموز الحالة — الفحص الأساسي؛ .expect(200)، .expect(401)، .expect(422).
  • شكل الاستجابة — تأكّد من وجود الحقول المطلوبة وامتلاكها للأنواع المتوقّعة.
  • الآثار الجانبية — بعد POST يُنشئ موردًا، استعلِم قاعدة البيانات مباشرةً (عبر المستودع المحقون) للتأكّد من استمرار الصف.
  • تطبيق الحُرّاس — أضِف دائمًا اختبارًا سلبيًا: ضرب مسار محمي بدون رمز يجب أن يُعيد 401.
لا تعتمد على ترتيب الاختبارات. يجب أن يُولّد كل اختبار (أو على الأقل كل كتلة describe) بياناته الأوّليّة المطلوبة وينظّفها بعد ذلك. الاختبارات التي تعتمد على تشغيل اختبارات سابقة أولًا هشّة وصعبة التصحيح.

الخلاصة

تستخدم اختبارات E2E في NestJS الدالة Test.createTestingModule() مع AppModule الحقيقية وSupertest لإطلاق طلبات HTTP على التطبيق الجاري. وجّه الاختبارات إلى قاعدة بيانات اختبار مخصّصة، وشغّل الترحيلات في beforeAll، ونظّف في afterAll. للمسارات المحميّة بالمصادقة، احصل على JWT في خطوة إعداد ثم أرفِقه عبر ترويسة Authorization. أضِف دائمًا اختبارات سلبية (بدون رمز → 401) وتحقّق من آثار قاعدة البيانات الجانبية لنقاط نهاية الطفرة.