كيفية عمل Unit-Test في NestJS
السلام عليكم ورحمة الله وبركاته
الفهرس
المقدمة
أحببت في هذا المقالة أن اشرح موضوع الـ 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
ما الحالات المختلفة التي يمكن أن تحدث في هذه الدالة ؟
مثل ما قلنا في مثال الطباخ، هنا يمكننا أن نفترض بعض الحالات التي يمكن أن تحدث مع الدالة ونرى كيف ستتصرف في كل حالة
- إذا أرجعت الـ
userModel.findById
بيانات المستخدم من قاعدة البيانات بشكل صحيح، هل ستقوم الدالة التي نختبرها بإرجاع نفس البيانات التي حصلت عليها ؟
لأنه قد يقوم شخص ما في الفريق بجعل الدالة getUserById
تقوم بتعديل البيانات قبل إرجاعها
مثلًا أضاف بيانات أخرى للمستخدم أو قام بحذف بعض البيانات منها
لذا يجب أن نتأكد من أن الدالة تقوم بإرجاع نفس البيانات التي حصلت عليها من userModel.findById
أو أنها تقوم بإرجاع البيانات بالشكل الذي نريده نحن ونتوقعه منها
- إذا لم ترجع الـ
userModel.findById
بيانات المستخدم بشكل صحيح، هل ستقوم الدالة بإرجاعNotFoundException
؟
لأنه قد يقوم شخص ما في الفريق بجعل الدالة 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;
};
الخطوات التي سنتبعها لاختبار أي دالة
- نعرف البيانات التي سنرسلها للدالة
- في حالة إذا كانت الدالة تستقبل بيانات
- نعرف شكل البيانات التي نتوقعها أن ترجع من الدالة
- نقوم بعمل
mocking
لأي دالة او مكتبة خارجية ليس ضمن ما نريده اختباره- مثل ما قمنا مع
mockUserModel
وmockUser
- أو نستخدم
jest.spyOn
أثناء الاختبار في حالة اذا أردت تغير الـimplementation
الافتراضي الذي وضعناه فيmockUserModel
- مثل ما قمنا مع
- نستدعي الدالة التي نريد اختبارها
- نختبر اذ ا كانت ترجع لنا نفس النتيجة التي نتوقعها دون تغير
- نختبر الامور الجانبية الاخرى باستخدام
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
وكيفية اختبار الدوال والتأكد من انها تعمل بشكل صحيح
وكيف انه يفيدنا حيث انه اذا قام احد زملائك في الشركة بتعديلات في الكود وقام بتغير الأمور بشكل خاطئ
أرجو أن تكون الفكرة وصلت لك وانك استفدت من هذا المقال وأن الشرح كان وافي لك