كيفية عمل Unit-Test في NestJS

السلام عليكم ورحمة الله وبركاته

وقت القراءة: ≈ 15 دقيقة

المقدمة

أحببت في هذا المقالة أن اشرح موضوع الـ unit-test في nestjs وكيفية عملها بشكل مبسط وسهل
ستجد العديد من المقالات والفيديوهات على الانترنت تشرح لك ما هو الـ unit-test وما هو الـ jest
لكنني أردت عمل مقالة تشرح تجربتي لعمل الـ unit-test في nestjs بشكل خاص لكن مع ذلك سأراعي الأشخاص الذي يريدون قراءه المقالة لفهم الـ unit-test في مثال عملي حقيقي

بالطبع سأتطرق لشرح ما هو الـ unit-test وما هو الـ jest وما هو مفهوم الـ mocking وما فائدته، لذا لا تقلق
لكني لن اتعمق كثيرًا لانني أريد أن أركز على كيفية عمل الـ unit-test في nestjs

ما هو الـ unit-test ؟

الـ unit-test هي اختبار شيء واحد فقط بشكل مستقل عن باقي الأشياء التي تعتمد عليها
بمعنى أننا لو أردنا اختبار وظيفة دالة معينة فإننا نقوم باختبار تلك الدالة بشكل مستقل تمامًا عن باقي الدوال

وإن كانت الدالة التي نريد اختبارها تعتمد على مكاتب خارجية أو دوال أخرى فإننا نزيف نواتج تلك الدوال والمكاتب الأخرى لنرى هل تعمل الدالة التي نريد اختبارها بشكل صحيح أم لا

async function getUserById(id: string): Promise<User> {
  const user = await userModel.findById(id);

  if (!user) throw new NotFoundException("User doesn't exist");

  return user;
}

فعلى سبيل المثال إذا قام زميلك في الشركة بعمل دالة getUserById تقوم بإرجاع مستخدم من قاعدة البيانات بناءً على id المستخدم وأنت تم تكليفك بعمل unit-test لهذه الدالة

أول شيء يجب أن تقوم به هو سؤال نفسك بعض الأسئلة

ما هي النتيجة التي تريد الحصول عليها من هذه الدالة ؟

أن تحضر لي بيانات المستخدم بالشكل الذي أتوقعه

فهنا يمكننا أن ننشيء object يضم البيانات التي نتوقعها أو نريدها أن تعود من الدالة

const expectedUser: User = {
  _id: 'user-id-123',
  fullName: 'user-name',
  email: '[email protected]',
  password: '1234567890',
};

القيم التي تضعها غير مهمة، لأنك لا تريد أن تختبر القيم بل تريد أن تختبر شكل المستخدم كيف تريده أن يرجع فقط

ما المكاتب أو الدوال أو الكلاسات الخارجية التي تعتمد عليها هذه الدالة ؟

هنا يمكنك أن تتخيل الأمر بأنك تريد أن تختبر الطباخ الذي يعمل لديك
وهو يعتمد على بعض الادوات، فأنت لا تريد أن تختبر الأدوات بل تريد أن تختبر الطباخ
لذا تقوم بافتراض بعض الحالات التي يمكن أن تحدث مع الأدوات وترى كيف سيتصرف الطباخ في كل حالة

هنا في دالة getUserById يمكننا أن نرى أن الدالة تعتمد على userModel ودالة findById الخاصة بها
وهذا ليس مهمًا لأننا لا نريد أن نختبر userModel بل نريد أن نختبر الدالة getUserById فقط لا غير
لذا هنا سنحاول عمل محاكاة للـ userModel ودالة findById الخاصة بها ونجعلها ترجع لنا البيانات بشكل صحيح

لاننا نريد أن نختبر الدالة getUserById ماذا ستفعل إذا كانت userModel تعمل بشكل صحيح وأرجعت القيم بشكل صحيح

عملية محاكاة أو تزييف طريقة عمل الدوال والمكاتب مثل الـ userModel تسمى بالـ mocking

ما الحالات المختلفة التي يمكن أن تحدث في هذه الدالة ؟

مثل ما قلنا في مثال الطباخ، هنا يمكننا أن نفترض بعض الحالات التي يمكن أن تحدث مع الدالة ونرى كيف ستتصرف في كل حالة

لأنه قد يقوم شخص ما في الفريق بجعل الدالة getUserById تقوم بتعديل البيانات قبل إرجاعها
مثلًا أضاف بيانات أخرى للمستخدم أو قام بحذف بعض البيانات منها

لذا يجب أن نتأكد من أن الدالة تقوم بإرجاع نفس البيانات التي حصلت عليها من userModel.findById
أو أنها تقوم بإرجاع البيانات بالشكل الذي نريده نحن ونتوقعه منها

لأنه قد يقوم شخص ما في الفريق بجعل الدالة getUserById تقوم بإرجاع null أو حتى BadRequestException أو أي شيء آخر غير الذي نريده

لذا يجب أن نتأكد من أن الدالة تقوم بإرجاع NotFoundException إذا لم تجد المستخدم

كل هذه الأمور يجب أن نفكر بها قبل أن نبدأ بعمل الـ unit-test للدالة

استخدام الـ jest لعمل الـ unit-test

الـ jest هي من أشهر المكاتب لعمل الـ unit-test
وتوفر لك طريقة بسيطة وأدوات ودوال متنوعة تساعدك على عمل الـ unit-test والـ mocking بشكل سهل ومنظم

سنستعرض مثال صغير لكيفية عمل الـ unit-test باستخدام الـ jest ثم نستكمل مثالنا الأساسي

لدينا دالة بسيطة تقوم بجمع رقمين موجبين فقط وإرجاع الناتج

function sumPositive(a: number, b: number): number {
  if (a < 0 || b < 0) throw new Error('a and b must be positive numbers');
  return a + b;
}

نريد أن نقوم بعمل unit-test لهذه الدالة

بمجرد النظر نستنتج أنها لا تعتمد على أي دوال أو مكاتب أو كلاسات أو غيرها من الأمور الخارجية لذا لا نحتاج لعمل mocking لأي شيء هنا
وأننا لدينا حالتين فقط يمكننا ان نختبرها، إما أن تقوم الدالة بإرجاع الناتج بشكل صحيح أو أن تقوم بإرجاع Error إذا كان أحد الرقمين سالبًا

describe('sumPositive', () => {
  test('should return the sum of two numbers', () => {
    const result = sumPositive(1, 2);
    expect(result).toBe(3);
  });

  test('should throw an error if one of the numbers is negative', () => {
    expect(() => sumPositive(1, -2)).toThrow(
      'a and b must be positive numbers'
    );
  });
});

ستلاحظ أن jest تقدم لك عدة دوال تساعدك لوصف وعمل unit-test بشكل واضح ومنظم والتأكد من الناتج المتوقع الذي تريده

من صمن تلك الدوال لديك describe وهي فقط تجمع اختبارات معينة تحت مسمى واحد كنوع من التنظيم
ودالة test وهي الدالة التي تبدأ من عندها عمل الاختبار للحالة المعينة التي تريد اختبارها
كل حالة نفكر فيها نضعها داخل test ونكتب وصف للحالة وما الذي نتوقعه منها

هنا لدينا حالتان فقط نريد اختبارهما، الحالة الأولى هي أن تعيد لنا الدالة sumPositive مجموع رقمين موجبين بشكل صحيح
الحالة الثانية هي أن تقوم الدالة بعمل Throw Error عندما يكون أحد الرقمين سالبًا

كما ترى لدينا أيضًا دالة مهمة جدًا تدعى expect والمسؤولة عن التأكد من شكل القيم والناتج النهائي هل هو ما نتوقعه أم لا

عندما تقوم بتشغيل الـunit-test ستجد أن الـ jest يقوم بتشغيل كل الحالات التي تريد اختبارها ويقوم بإخبارك بالحالات التي نجحت والحالات التي فشلت بشكل واضح

 PASS  ./sumPositive.test.ts
  sumPositive
    ✓ should return the sum of two numbers (1 ms)
    ✓ should throw an error if one of the numbers is negative (1 ms)

لن أتعمق كثيرًا في الدوال التي تقدمها jest لأنها كثيرة ومتنوعة، لذا يمكنكم تفقد الـ docs من الرابط التالي https://jestjs.io/docs/api لترى كل الدوال التي تقدمها jest

تذكر نحن نقوم باختبار كل الحالات التي نتوقعها من الدالة بحيث إن قام أحد ما بتغير الكود أو تعديله سيقوم بتشغيل الـ unit-test ليتأكد من أنه لم يغير في وظيفة الدالة الأساسية

بحيث أنه لو حذف سطر الشرط التي نتأكد هل القيم موجبة أم لا

function sumPositive(a: number, b: number): number {
  return a + b;
}

ثم قام بتشغيل الـ unit-test سيقوم الـ jest بإخباره أن هناك حالة فشلت وهي حالة إذا كانت القيم سالبة السالبة

FAIL  src/sumPositive.test.ts
  sumPositive
    ✕ should throw an error if one of the numbers is negative (1 ms)

  ● sumPositive › should throw an error if one of the numbers is negative

    a and b must be positive numbers

      10 |   test('should throw an error if one of the numbers is negative', () => {
      11 |     expect(() => sumPositive(1, -2)).toThrow(
    > 12 |       'a and b must be positive numbers'
         |       ^
      13 |     );
      14 |   });

      at sumPositive (src/sumPositive.ts:3:11)
      at Object.<anonymous> (src/sumPositive.test.ts:12:7)

تطبيق الـ unit-test في nestjs

حسنًا لنبدأ بتطبيق مثالنا العملي للـ unit-test في nestjs

هيكل الملفات وترتيبها لدينا بهذا الشكل

src
├── app.module.ts
├── main.ts
├── modules
│   └── users
│       ├── __mocks__ # we will create it to put the mocks in it
│       │   └── user.mock.ts # mock the user data
│       │   └── user.model.mock.ts # mock the user model
│       ├── dto
│       │   ├── create-user.dto.ts
│       │   └── update-user.dto.ts
│       ├── model
│       │   └── user.model.ts
│       ├── controller # we will not test it in this article
│       │   └── users.controller.spec.ts
│       │   └── users.controller.ts
│       ├── service
│       │   └── users.service.spec.ts # <---- we are here :)
│       │   └── users.service.ts # service that we will test
│       └── users.module.ts

في مثالنا الأساسي كان لدينا دالة getUserById التي تقوم بإرجاع مستخدم من قاعدة البيانات بناءً على id المستخدم
هذه الدالة بداخل UserService

// users.service.ts -> UsersService -> getUserById
async function getUserById(id: string): Promise<User> {
  const user = await userModel.findById(id);

  if (!user) throw new NotFoundException("User doesn't exist");

  return user;
}

عمل mocking لتزييف عمل الـ userModel

أول شيء ستلاحظه في الدالة getUserById هي أنها تعتمد على دالة findById من userModel
لذا سنقوم بعمل mocking لـ userModel ودالة findById الخاصة بها
لكن قبل هذا سنقوم بإنشاء دالة تقوم بإرجاع بيانات المستخدم بالشكل المطابق للـ schema الخاصة به

سنقوم بإنشاء مجلد يدعى __mocks__ وسنضع فيه كل ما نريد عمل mocking له كنوع من التنظيم

// __mocks__/user.mock.ts
import { UserDocument } from '../model/user.model';

export const userMock = (): UserDocument => {
  return {
    _id: 'user-id-123',
    fullName: 'user-name',
    email: '[email protected]',
    password: '1234567890',
  } as UserDocument;
};

هنا لدينا دالة بسيطة تقوم بإرجاع بيانات المستخدم بقيم افتراضية
نحن في الـ unit-test لا نهتم بالقيم ذاتها بل نهتم بشكل البيانات فقط

الآن نريد عمل mocking لـ userModel ودالة findById الخاصة بها
لأننا لا نريد أن نختبر userModel أو الدالة findById بل نريد أن نختبر الدالة getUserById فقط لا غير
لذا سنجعلها تقوم بإرجاع البيانات بالشكل الذي نتوقع

// __mocks__/user.model.mock.ts
import { UserDocument } from '../model/user.model';
import { userMock } from './user.mock';

export const mockUserModel = {
  findById: jest
    .fn()
    .mockImplementation((_id) => Promise.resolve(userMock())),
  // you can add more methods here and mock them
};

هنا لدينا object يدعى mockUserMock والذي سيكون بديل أو محاكاة للـ UserModel
بداخله لدينا findById وهي الدالة التي نريد عمل mocking لها لانها الدالة التي تعتمد عليها الدالة getUserById

يمكنك عمل كلاس بدلًا من object لكنني أفضل الـ object لأنه أسهل وأبسط في حالة الـ object يحتوي على constructor على سبيل المثال وتريد عمل mocking له فعليك استخدام الـ class بدلًا من الـ object، أنت من تقرر ماذا تستخدم بحسب ما يناسبك

findById: jest.fn().mockImplementation((_id) => Promise.resolve((userMock()))),
// or findById: jest.fn().mockResolvedValue(userMock()),

هنا قمنا باستخدام jest.fn() لإنشاء دالة mock جديدة خاصة بـ findById ثم قمنا باستخدام mockImplementation لعمل implementation مزيف لدالة
وجعلناها ترجع لنا القيمة التي نريدها وتلك القيمة هي userMock() البيانات المزيفة بشكل الـ schema الخاصة بالمستخدم

ستجد دوال متنوعة غير الـ mockImplementation تقدمها jest مثل mockResolvedValue
والتي كما يوحي الاسم تزيف الناتج الراجع من الدالة
ولا نحتاج لأن نستقبل شيء ما أو الـ id لأننا لا نهتم بالـ id بل نهتم بشكل البيانات الراجع من الدالة والذي سيكون ثابت لذا سنستخدم في هذه الحالة jest.fn().mockResolvedValue(userMock())

لاننا كما قلنا لا نريد أن نختبر userModel أو الدالة findById بل نريد أن نختبر الدالة getUserById فقط لا غير
لذا نحتاج منها فقط بأن ترجع لنا بيانات مزيفة لكي نستطيع أن نبدأ في الاختبار وكما تلاحظ فأننا نرجع نفس البيانات بغض النظر عن الـ id الذي يتم إرساله للدالة

داخل الـ mockUserModel يمكنك وضع الدوال التي تريد عمل mocking لها

// __mocks__/user.model.mock.ts
import { UserDocument } from '../model/user.model';
import { userMock } from './user.mock';

export const mockUserModel = {
  findById: jest.fn().mockResolvedValue(userMock()),
  // we can add more methods here and mock them
  // create: jest.fn().mockResolvedValue(userMock()),
  // findOne: jest.fn().mockResolvedValue(userMock()),
  // findByIdAndDelete: jest.fn().mockResolvedValue(userMock())
};

كما ترى وضعنا دالة findById وقمنا بعمل mocking لها، الأمر بهذه البساطة كما ترى
يمكنك ان تضع اي دالة هنا تريد تغير الـ implementation الخاص بها

الآن لدينا الدالة التي نريدها والتي تعمل بشكل مزيف وتقوم بإرجاع البيانات بالشكل الذي نريده

ملحوظة: الـ mocking الذي قمنا به للدوال سيكون هو الـ mocking الافتراضي لتلك الدوال، نستطيع في أي وقت تغير الـ mocking الافتراضي للدوال اثناء اختبار أي حالة باستخدام jest.spyOn وتغير الـ implementation كما تشاء سواء لمرة واحدة ثم يعود للـ mocking الافتراضي للدوال أو تغيره بشكل دائم

إعداد الـ test module

الآن وبعد انتهائنا من عمل mocking للأشياء الخارجية التي تعتمد عليها الدالة getUserById مثل userModel ودالة findById الخاصة بها

نبدأ في اعداد الـ module الخاص بالـ unit-test لكي نبدأ باختبار الـ UserService وهذا ما سنقوم به في الـ beforeAll وهو أننا سنقوم بإنشاء testing module ونقوم بإعداد الـ providers الذي يحتاجها

الـ beforeAll هي دالة تقدمها الـ jest تقوم بتنفيذ الكود الذي تضعه فيها قبل أن تبدأ بتشغيل الـ unit-test

// users.service.spec.ts

let userService: UsersService; // the service that we will test
let userModel: Model<UserDocument>; // the user model that we will mock

beforeAll(async () => {
  // create a testing module
  const module: TestingModule = await Test.createTestingModule({
    providers: [
      UsersService, // the service that we will test
      {
        provide: getModelToken(User.name),
        useValue: mockUserModel, // use the mock implementation of the user model
        // if mockUserModel was a class you should use useClass instead of useValue
      },
    ],
  }).compile();

  // set the user service and user model
  userService = module.get<UsersService>(UsersService);
  userModel = module.get<Model<UserDocument>>(getModelToken(User.name));
});

بدأ الـ unit-test

الآن سيبدأ العمل الفعلي للـ unit-test
لكن سأعرض لك بعض الخطوات التي ستساعدك في اختبار كل دالة

لنتذكر الدالة التي نريد أن نختبرها

// users.service.ts -> UsersService -> getUserById
async function getUserById(id: string): Promise<User> {
  const user = await userModel.findById(id);

  if (!user) throw new NotFoundException("User doesn't exist");

  return user;
}

ولنتذكر الـ mockUserModel الذي قمنا به

// __mocks__/user.model.mock.ts
import { UserDocument } from '../model/user.model';
import { userMock } from './user.mock';

export const mockUserModel = {
  findById: jest.fn().mockResolvedValue(userMock()),
};

وأيضًا لا ننسى الـ userMock الذي قمنا بإنشائه

// __mocks__/user.mock.ts
import { UserDocument } from '../model/user.model';

export const userMock = (): UserDocument => {
  return {
    _id: 'user-id-123',
    fullName: 'user-name',
    email: '[email protected]',
    password: '1234567890',
  } as UserDocument;
};

الخطوات التي سنتبعها لاختبار أي دالة

  1. نعرف البيانات التي سنرسلها للدالة
    • في حالة إذا كانت الدالة تستقبل بيانات
  2. نعرف شكل البيانات التي نتوقعها أن ترجع من الدالة
  3. نقوم بعمل mocking لأي دالة او مكتبة خارجية ليس ضمن ما نريده اختباره
    • مثل ما قمنا مع mockUserModel و mockUser
    • أو نستخدم jest.spyOn أثناء الاختبار في حالة اذا أردت تغير الـ implementation الافتراضي الذي وضعناه في mockUserModel
  4. نستدعي الدالة التي نريد اختبارها
  5. نختبر اذ ا كانت ترجع لنا نفس النتيجة التي نتوقعها دون تغير
  6. نختبر الامور الجانبية الاخرى باستخدام expect

اختبار دالة getUserById

ملحوظة صغيرة قبل أن نبدأ
لنفترض أن زميلك في الشركة قال لك ان الـ findById تقوم بإرجاع الـ password
والدالة getUserById المفترض أنها لن ترجعه

فنحن نريد منك أن تختبر الدالة getUserById وتتأكد من أنها لا ترجع الـ password

test('should get a user by id', async () => {
  // define the expected result
  const expectedUser: UserDocument = {
    _id: userMock()._id,
    fullName: userMock().fullName,
    email: userMock().email,
    // we don't want to return the password
  };

  // call the service method to test its behavior
  const result = await userService.getUserById(userMock()._id);

  // test the result
  expect(result).toEqual(expectedUser); // should return the same result that we expect

  // extra tests
  // test if the method was called with the correct data
  expect(mockUserModel.findById).toHaveBeenCalledWith(userMock()._id);
});

سأقوم بمراجعة كل سطر ولماذا استخدمناه كيف سيفيدنا

أولًا استخدمنا دالة test لبدأ الاختبار وقمنا بكتابة وصف أو عنوان توضيحي لهذا الاختبار
ثم عرفنا شكل البيانات التي نتوقعه ان يرجع وستلاحظ اننا لا نريد ان تقوم الدالة بإرجاع الـ password
بالتالي عندما نستدعي الدالة getUserById فنحن نتوقع الا ترجع الـ password لنا

وفي حالة أنها أرجعته فسيقوم الـ jest بإخبارنا أن الاختبار فشل
سؤال هل سيفشل الاختبار الآن ام سينجح ؟
فكر جيدًا وراجع الدالة الـ getUserById وما قمنا به سابقًا في الـ mockUserModel والـ userMock

وتذكر أننا في الـ mockUserModel جعلنا الدالة findById ترجع لنا userMock

findById: jest.fn().mockResolvedValue(userMock()),

والـ userMock يحتوي على الـ password
هكذا قمنا باعداد الـ mocking الخاصة بنا

ونريد أن نختبر هل ستقوم الدالة getUserById بإرجاع الـ password أم لا
نحن نتوقع ألا تفعل
لكن اذا نظرنا للدالة getUserById سنجد أنها ترجع لنا ناتج الدالة findById بشكل مباشر دون حذف الـ password

// users.service.ts -> UsersService -> getUserById
async function getUserById(id: string): Promise<User> {
  const user = await userModel.findById(id);

  if (!user) throw new NotFoundException("User doesn't exist");

  return user;
}

لذا الاجابة ستكون ان الاختبار سيفشل

FAIL  src/users/service/users.service.spec.ts
  UsersService
    ✕ should get a user by id (2 ms)

  ● UsersService › should get a user by id

    expect(received).toEqual(expected) // deep equality

    Expected: {"_id": "user-id-123", "fullName": "user-name", "email": "[email protected]"}
    Received: {"_id": "user-id-123", "fullName": "user-name", "email": "[email protected]" "password": "1234567890"}

      27 |   // test the result
    > 28 |   expect(result).toEqual(expectedUser);
         |                         ^
      29 |

ماذا سنفعل الآن ؟
تقوم بالرجوع لزميلك وتقول له ان الاختبار فشل وتقول له ان الدالة getUserById تقوم بإرجاع الـ password
لذا في هذه الحالة سيتم مراجعة الدالة getUserById وتعديلها لتقوم بحذف الـ password قبل ان ترجع الناتج

// users.service.ts -> UsersService -> getUserById
async function getUserById(id: string): Promise<User> {
  const user = await userModel.findById(id);

  if (!user) throw new NotFoundException("User doesn't exist");
  delete user.password; // <---- add this line to remove the password

  return user;
}

بعد اصلاح المشكلة ستجد ان الاختبار سينجح

 PASS  src/users/service/users.service.spec.ts
  UsersService
    ✓ should get a user by id (1 ms)

اختبار دالة getUserById في حالة إذا لم تجد المستخدم

الآن سنقوم بعمل اختبار للحالة الأخرى وهي إذا لم تجد المستخدم فنحن نتوقع أن تقوم الدالة بإرجاع NotFoundException

هنا سنقوم بعمل mocking للـ findById ونجعلها ترجع null
لانها حاليا ترجع لنا userMock كما عرفناها في الـ mockUserModel

لكن في هذه الحالة الأمر مختلف نريد أن نختبر الدالة getUserById ونتأكد من أنها تقوم بإرجاع NotFoundException
في حالة عدم وجود المستخدم

لذا سنقوم بعمل mocking للـ findById وجعلناها ترجع null لنحاكي تلك الحالة
لنغير الـ implementation الافتراضي الذي وضعناه للـ findById سنستخدم jest.spyOn
لأن jest.spyOn تستطيع تغير أي implementation كما تشاء سواء في أي وقت سواء تغيره لمرة واحدة ثم يعود للـ mocking الافتراضي للدالة أو تغيره بشكل دائم

test('should throw a NotFoundException if user not found', async () => {
  // we used jest.spyOn to change the default implementation of the dependency (methods in mockUserModel)
  // we use `mockResolvedValueOnce` not `mockResolvedValue` because we want to back to the default implementation after the test
  // if we use `mockResolvedValue` it will change the default implementation forever
  jest.spyOn(userModel, 'findById').mockResolvedValueOnce(null);

  // we didn't put the result in a variable or await the expect
  // because we want to test that the method throw an error
  await expect(userService.getUserById(userMock()._id)).rejects.toThrow(
    NotFoundException
  );
  expect(mockUserModel.findById).toHaveBeenCalledWith(userMock()._id);
});

استخدام jest.spyOn سهل جدًا وبسيط فقط نحدد الكلاس والدالة المتواجدة داخله ثم نغير الـ implementation

jest.spyOn(userModel, 'findById').mockResolvedValueOnce(null);

ولاحظ اننا استخدمنا mockResolvedValueOnce وليس mockResolvedValue هذه المرة
لاننا نريد ان نعود للـ implementation الافتراضي بعد اختبار تلك الحالة
لو استخدمنا mockResolvedValue سيتم تغير الـ implementation الافتراضي للدالة بشكل دائم وهذا سيأثر على باقي الاختبارات التي تعتمد على الـ implementation الافتراضي المتواجد داخل mockUserModel

بعد ذلك نقوم باستدعاء الدالة getUserById ونتأكد من أنها تقوم بإرجاع NotFoundException

await expect(userService.getUserById(userMock()._id)).rejects.toThrow(
  NotFoundException
);

حيث rejects تقوم باستقبال أي throw exception وتقوم toThrow بالتأكد من أنها من نوع NotFoundException

ستلاحظ اننا لم نضع userService.getUserById(userMock()._id) داخل متغير result كما كنا نفعل
بهذه الطريقة

const result = await userService.getUserById(userMock()._id); // this will throw an exception
// so it will not continue to the next line
// because it will throw an exception and stop the execution
expect(result).rejects.toThrow(NotFoundException);

لان الدالة getUserById الآن تقوم بعمل throw exception وليس بإرجاع ناتج
والـ throw exception سيوقف تنفيذ الكود ولن ينفذ باقي الأسطر بالتالي لن يصل للـ expect

لذا نستخدمها بتلك الطريقة await expect(/* calling the method */).rejects.toThrow(exception);
لكي نقوم باستقبال الـ throw exception بشكل فوري ثم نتأكد هل الـ exception من نوع NotFoundException أم لا

ملحوظة: في المستقبل اذا قام احد زملائك في الشركة بتعديلات في الكود وقام فجأة بسبب ما أو عن طريق الخطأ بتغير الـ exception المتوقع للدالة getUserById التي في الـ UsersService من NotFoundException إلى BadRequestException سيقوم الـ unit-test بإخباره أن هناك حالة فشلت وسيوضح له انه كان يتوقع NotFoundException ولكن الدالة getUserById قامت بإرجاع BadRequestException

ألق نظرة أخرى على الاختبار

test('should throw a NotFoundException if user not found', async () => {
  jest.spyOn(userModel, 'findById').mockResolvedValueOnce(null);

  await expect(userService.getUserById(userMock()._id)).rejects.toThrow(
    NotFoundException
  );

  expect(mockUserModel.findById).toHaveBeenCalledWith(userMock()._id);
});

يمكننا كتابة نفس الاختبار بطريقة اخرى باستخدام try catch

test('should throw a NotFoundException if user not found', async () => {
  jest.spyOn(userModel, 'findById').mockResolvedValueOnce(null);
  try {
    await userService.getUserById(userMock()._id); // this will throw an exception
  } catch (exception) {
    expect(exception).toBeInstanceOf(NotFoundException);
    expect(mockUserModel.findById).toHaveBeenCalledWith(userMock()._id);
  }
});

ستلاحظ انها نفس الطريقة العادية لكن هذه المرة استخدمنا try catch
وستقوم الـ catch بالتقاط الـ throw exception وتخزينه في متغير exception
ثم ببساطة نستطيع أن نرى اذا كان الـ exception من نوع NotFoundException أم لا

خاتمة

أظن أنه سنختفي بهذا القدر من الـ unit-test وفهم الفكرة العامة له
كنت أود أن اشرح كيفية عمل الـ unit-test وكيفية تطبيقها في nestjs بشكل عملي ومباشر
حاولت ان اوضح الفكرة الاساسية ولما نستعملها وكيف نستعملها
وكيفية عمل الـ mocking وكيفية تطبيقها في nestjs
وكيفية اختبار الدوال والتأكد من انها تعمل بشكل صحيح
وكيف انه يفيدنا حيث انه اذا قام احد زملائك في الشركة بتعديلات في الكود وقام بتغير الأمور بشكل خاطئ

أرجو أن تكون الفكرة وصلت لك وانك استفدت من هذا المقال وأن الشرح كان وافي لك