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

اختبار الوحدة للخدمات والمتحكّمات

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

اختبار الوحدة للخدمات والمتحكّمات

تتحقّق اختبارات الوحدة من صنف واحد في عزلة تامّة — بلا قواعد بيانات حقيقية، ولا خوادم HTTP، ولا تبعيات خارجية. يُشحن NestJS مع Jest مُهيَّأً مسبقًا، ويوفّر Test.createTestingModule()، وهو مصنع وحدات خفيف الوزن يُجمِّع فقط ما تُعلنه، مُستبدِلًا كلّ شيء حقيقي بـموفّري وهميين من تصميمك. والنتيجة اختبارات تعمل في أجزاء من الثانية وتُعطي تغذية راجعة دقيقة وقابلة للاستنساخ.

لماذا نعزل بالنماذج الوهمية؟

  • السرعة — بلا إدخال/إخراج، تنتهي كامل مجموعة الاختبارات في ثوانٍ.
  • الدقّة — الاختبار الفاشل يُشير إلى صنف واحد لا سلسلة تكاملية.
  • الحتمية — تتحكّم في قيمة إرجاع كلّ تبعية؛ مشكلات الشبكة المتقلّبة لن تكسر التكامل المستمر أبدًا.
  • ضغط التصميم — الكود الذي يصعب اختباره عادةً يصعب صيانته؛ الصعوبة هنا إشارة.

إنشاء وحدة اختبار معزولة

يقبل Test.createTestingModule() نفس كائن البيانات الوصفية الخاص بـ@Module(). أعلن فقط الصنف قيد الاختبار وقدّم بدائل وهمية لكلّ تبعية باستخدام صياغة الموفّر المخصّص (useValue):

import { Test, TestingModule } from '@nestjs/testing'; import { UsersService } from './users.service'; import { getRepositoryToken } from '@nestjs/typeorm'; import { User } from './user.entity'; describe('UsersService', () => { let service: UsersService; const mockRepo = { findOne: jest.fn(), save: jest.fn(), create: jest.fn(), }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ UsersService, { provide: getRepositoryToken(User), useValue: mockRepo, }, ], }).compile(); service = module.get<UsersService>(UsersService); }); afterEach(() => jest.clearAllMocks()); it('should be defined', () => { expect(service).toBeDefined(); }); });
compile() غير متزامن. استخدم await دائمًا داخل استدعاء رد نداء beforeEach غير المتزامن. الوحدة المُجمَّعة مؤقّتة — يحصل كلّ اختبار على نسخة جديدة، ممّا يمنع تسرّب الحالة بين الاختبارات.

اختبار توابع الخدمة مع الجواسيس

استخدم jest.fn() للإعلان عن دالّة وهمية، وmockResolvedValue / mockReturnValue للتحكّم فيما تُعيده في اختبار محدّد. ثمّ جسّس على النموذج الوهمي للتأكّد من أنّه استُدعي بالوسائط الصحيحة:

it('should return a user when found', async () => { const fakeUser = { id: 1, email: 'a@example.com' }; mockRepo.findOne.mockResolvedValue(fakeUser); const result = await service.findById(1); expect(result).toEqual(fakeUser); expect(mockRepo.findOne).toHaveBeenCalledWith({ where: { id: 1 } }); expect(mockRepo.findOne).toHaveBeenCalledTimes(1); }); it('should throw NotFoundException when user is missing', async () => { mockRepo.findOne.mockResolvedValue(null); await expect(service.findById(99)).rejects.toThrow('User not found'); });

اختبار وحدة المتحكّم

تعتمد المتحكّمات على الخدمات لا على المستودعات. قدّم خدمة وهمية بـuseValue، جمِّع الوحدة، وادعُ توابع المتحكّم مباشرةً — بلا عبء HTTP:

import { UsersController } from './users.controller'; describe('UsersController', () => { let controller: UsersController; const mockUsersService = { findById: jest.fn(), create: jest.fn(), }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [UsersController], providers: [ { provide: UsersService, useValue: mockUsersService }, ], }).compile(); controller = module.get<UsersController>(UsersController); }); it('should call service.findById and return the result', async () => { const user = { id: 1, email: 'a@example.com' }; mockUsersService.findById.mockResolvedValue(user); const result = await controller.findOne('1'); expect(result).toEqual(user); expect(mockUsersService.findById).toHaveBeenCalledWith(1); }); });

jest.spyOn للمحاكاة الجزئية

عندما تريد محاكاة تابع واحد فقط على كائن حقيقي — تاركًا الباقي سليمًا — استخدم jest.spyOn(object, 'methodName'). هذا مثالي لاختبار التفاعلات بين التوابع داخل الصنف نفسه:

it('should call hashPassword before saving', async () => { const spy = jest.spyOn(service as any, 'hashPassword') .mockResolvedValue('hashed-pw'); mockRepo.create.mockReturnValue({ email: 'b@b.com' }); mockRepo.save.mockResolvedValue({ id: 2, email: 'b@b.com' }); await service.create({ email: 'b@b.com', password: 'plain' }); expect(spy).toHaveBeenCalledWith('plain'); });
اجعل الكائنات الوهمية ضحلة. أعلن فقط التوابع التي يستدعيها الصنف قيد الاختبار فعليًا. الكائن الوهمي الأبسط يُوضح سطح التبعيات صراحةً، ورسالة الخطأ عند غياب تابع تكون أكثر إفادةً بكثير.
لا تختبر تفاصيل التنفيذ الداخلية أبدًا. أكّد على النتائج القابلة للملاحظة (قيم الإرجاع، والأخطاء المُرمَاة، والمتعاونين الذين استُدعوا). التأكيد على الكيفية الداخلية لبنية التابع يجعل الاختبارات هشّة — تنكسر عند إعادة هيكلة آمنة.

الخلاصة

يُجمِّع Test.createTestingModule() وحدة NestJS معزولة للاختبار. استبدل كلّ تبعية حقيقية بنموذج وهمي من useValue مبنيّ من دوالّ jest.fn()، ثمّ استخدم mockResolvedValue للتحكّم في قيم الإرجاع وtoHaveBeenCalledWith للتحقّق من التفاعلات. طبّق النمط نفسه على المتحكّمات — قدّم خدمة وهمية بدلًا من الحقيقية. استخدم jest.spyOn للمحاكاة الجزئية المحدّدة. ينبغي أن تكون اختبارات الوحدة سريعة ومعزولة ومركّزة على صنف واحد في كلّ مرّة.