رهام رفیعی تهرانی
رهام رفیعی تهرانی
خواندن ۸ دقیقه·۱ سال پیش

همه چیز درباره unit test در Angular - بخش چهارم

در این سری مقالات درباره تست های فریم ورک Angular صحبت میکنیم.

در بخش اول از کلیات تست گفتیم و برای پایپ ها، سرویس ها و کامپوننت ها isolated test نوشتیم.

در بخش دوم تست های shallow integrated رو برای کامپوننت نوشتیم.

در بخش سوم تست های deep integration رو برای کامپوننت و سرویس مون نوشتیم.

در این بخش میخوایم تمپلت رو دقیق تر تست کنیم.


نمونه کد های این سری مقالات رو میتونید در گیت هاب مطالعه کنید یا پروژه رو دانلود و نصب کنید.

آدرس گیت هاب این سری مقالات : https://github.com/rohamtehrani/angular-unit-tests


یک نگاهی به کامپوننت های Hero و Heroes و ارتباط شون موقع حذف یک Hero بندازیم:

در تمپلت کامپوننت HeroComopnent یک دکمه داریم با محتوای x که کلیک کردنش باعث اجرای تابع onDeleteClick میشه:

تابع onDeleteClick یک event به کامپوننت parent اعلام میکنه و چیزی داخلش نمیفرسته. کامپوننت بالاسرش هم HeroesComponent هستش:

بریم تمپلت کامپوننت HeroesComponent رو ببینیم که چطوری event صدا زده شده از app-hero رو میگیره و یک تابع داخل خودش به اسم delete رو صدا میزنه.

دقت کنید که کامپوننت app-hero هیچ دیتایی به کامپوننت بالاسرش نفرستاد. کامپوننت Heroes خودش اطلاعات خودش رو به تابع خودش ارسال کرد. به درستی یا غلطی این ارتباط در این مقاله کاری نداریم.

ببینیم تابع نهایی delete چه میکنه؟

اول hero ارسال شده رو از لیست heroes حذف میکنه.

بعد اطلاعات hero رو میگیره و تابع deleteHero از سرویس heroService رو صدا میکنه.


سناریوی تست مون که برای کامپوننت Heroes میخوایم بنویسیم اینه:

میخوایم ببینیم که با کلیک روی دکمه x در child component ش که یک app-hero هست ، آیا تابع delete از کامپوننت اصلی با پارامتر درست صدا زده میشه یا نه.

درگیر جزییاتی که در بخش های قبلی توضیح دادیم نمیشیم. میدونیم چطوری باید سرویس رو mock کنیم و شرایط اولیه تست ( مثل لیست HEROES ) رو بسازیم و .... مستقیم میریم سراغ تست.

اگر لازم می بینید دوباره بخش های قبل رو مطالعه کنید

تست رو در فایل heroes.component.deep.spec.ts که در بخش های قبل ساختیم، اضافه میکنیم.

فعلا میخوایم بخشی از تست رو پیاده سازی کنیم که دسترسی به یک instance از کامپوننت app-hero ایجاد می کنیم و روی دکمه کلیک می کنیم و ببینیم آیا تابع delete در کامپوننت اصلی صدا زده میشه یا خیر:


  • به کمک spyOn صدا زدن تابع delete در instance کامپوننت اصلی رو بررسی میکنیم.
  • یک mock از روی سرویس HeroService رو با مقادیر HEROES پر کردیم.
  • به کمک تابع detectChanges ، تابع ngOnInit رو به صورتی صدا زدیم که در اکوسیستم فریم ورک Angular صدا زده میشه و همه تغییرات اتفاق می افته.
  • لیستی از همه کامپوننت های app-hero ساخته شده گرفتیم.
  • در اولین app-hero ، به دنبال دکمه گشتیم (فقط یک دکمه داره برای ارسال event حذف )
  • روی دکمه مربوطه، event کلیک رو فعال کردیم و آبجکتی شبیه به $event پاس دادیم که به تابع هندلر کلیک مون پاس داده میشه
  • در انتها بررسی میکنیم ببینیم تابع delete صدا زده شده یا خیر

تست با موفقیت پاس میشه

حالا اگر انتظار تست مون رو عوض کنیم و عمدا پارامتر اشتباهی رو تست کنیم، می بینیم که تست اشتباه مون رو متوجه میشه و failed میشه:


همین طور اگر بجای کلیک ، فوکوس روی button رو انجام بدیم، می بینیم که کدمون failed میشه:

بنابر این تست مون رو درست نوشتیم.


مدل دیگه ای که میتونیم تست رو بنویسیم اینه که بجای query گرفتن روی کامپوننت app-hero، پیدا کردن دکمه و کلیک کردن روی دکمه حذف، با صدا زدن تابع emit از متغیر delete (اسم eventEmitter کامپوننت app-hero) ، delete event رو raise کنیم:

در واقع undefined رو برای این ارسال میکنیم که موقع emit در کد واقعی هیچ پارامتری توسط eventEmitter بر نمی گرده به کامپوننت اصلی.


مدل دیگه ای که میتونیم تست رو بنویسیم اینه که از امکان event raise در debugElement استفاده کنیم:


تست بعدی که میخوایم بنویسیم برای تست input هستش. میخوایم نوشتن یک اسم در input text رو شبیه سازی کنیم و ببینیم دکمه add رو میزنیم، آیا hero جدید به لیست اضافه میشه یا خیر.

اول یک نگاهی به کدها بندازیم ببینیم فرایند اضافه کردن hero جدید چطوری اتفاق می افته.

اول بریم سراغ تمپلت heroes.comopnent.ts


  • یک input داریم که رفرنس heroName داره . باید بهش دسترسی پیدا کنیم و بهش مقدار بدیم.
  • یک دکمه داریم برای add که اولین دکمه تمپلت هست. باید پیداش کنیم و روش کلیک کنیم.
  • کلیک روی دکمه add تابع add از کلاس کامپوننت رو فراخوانی میکنه

حالا بریم ببینیم در کلاس کامپوننت چه خبره:

  • تابع add رو داریم که مقدار name رو میگیره و تابع addHero از heroService رو صدا میکنه
  • باید یک تغییری در تابع addHero در آبجکت mock که بجای سرویس اصلی HeroService صداش میکنیم ایجاد کنیم که مثل تابع addHero در سرویس اصلی کار کنه.


بریم تستش رو بنویسیم و ببینیم نکته های بالا رو چطوری پیاده سازی کردیم:

  • لیست HEROES رو به عنوان خروجی getHeroes ست کردیم.
  • اجازه دادیم تغییرات در فضای ماژول مون اتفاق بیفته.
  • یک اسم مشخص ساختیم.
  • تابع addHero در آبجکت mock رو شبیه سازی کردیم، طوری که بعد از گرفتن نتیجه یک آبجکت ساخته شده با اسمی که مشخص کردیم برمیگردونه.
  • به المنت input در debugElement دسترسی پیدا کردیم.
  • به اولین دکمه تمپلت در debugElement دسترسی پیدا کردیم.
  • مقدار input رو عوض کردیم.
  • روی دکمه addButton کلیک کردیم و مقدار null رو پاس دادیم. همون طور که در کد اصلی هم مقداری که برمیگرده آبجکت event نیست . بلکه مقدار داخل input هست که در خط قبلی ست شده.
  • اجازه میدیم تغییرات اکوسیستم تست اتفاق بیفته
  • نگاه میکنیم ببینیم داخل ul اسمی که ما تنظیم کردیم پیدا میشه یا نه.

بخش Assert تست رو به روش های دیگه هم میشه پیاده سازی کرد که دقیق تر هم باشه. صرفا برای این اینطوری نوشتم که کد متفاوتی رو ببینیم.



بیایم نگاه دوباره ای روی mock کردن سرویس های inject شده به کامپوننت بندازیم.

کلاس کامپوننت hero-detail.component.ts رو در نظر بگیرید:

  • سه تا سرویس inject شده داخلش.
  • از سرویس heroService ، توابع getHero و updateHero استفاده شده.
  • از سرویس location ، تابع back استفاده شده.
  • از سرویس route که یه کم پیچیده تره، متغیر spanshop و ...

میخوایم آبجکت mock هر سه سرویس رو ببینیم:

عدد ۳ در mockActivatedTest دل به خواهه.

این پیاده سازی اولیه تست برای این کامپوننت ( در فایل hero-detail.component.spec.ts ) حدودا اینطوریه:


حالا یه نگاهی به کلاس کامپوننت hero detail بندازیم:

داخل تابع ngOnInit تابع getHero صدا زده شده.

تابع getHero هم بعد از گرفتن پارامتر id که شبیه سازیش کردیم، مستقیما رفته heroService رو صدا زده. ظاهرا شناسه ای رو که به عنوان پارامتر آدرسش گرفته ، به این تابع پاس میده و یک آبجکت بهش برگردونده میشه.

بنابر این در بخش beforeEach باید یه فکری به حال getHero در آبجکت mockHeroService بکنیم و تابع اصلی رو شبیه سازی کنیم:


اما با اجرای تست می بینیم که failed شده بخاطر تعریف نشدن ngModel:


سریع ترین روش برای حل این مشکل اینه که FormsModule رو داخل تنظیمات ماژول تستی import کنیم:

و خطای ngModel رفع میشه


حالا اگر بخوایم صحیح رندر شدن این کامپوننت رو بررسی کنیم، اول یه نگاهی به تمپلتش میندازیم:

می بینیم که اسم hero داخل تگ <h2> به صورت uppercase قرارگرفته.

بریم تستش کنیم. با توجه به اینکه تابع getHero در mockHeroService مقدار SuperDude رو برمیگردونه، انتظار داریم مقدار SUPERDUDE رو در <h2> ببینیم:

تست مون درست کار میکنه.

میتونیم با تغییر دادن اسم مورد انتظارمون صحت تست مون رو بیشتر بررسی کنیم.


آخرین نکته ای که در این سری مقالات بهش می پردازیم بحث async test هست.

فرض کنید تابع save در کامپوننت hero detail رو به صورت زیر نوشته باشیم:

وقتی برای این تابع تست های معمولی رو بنویسیم که قبلا نوشتیم به خطا میخوریم:

چرا ؟

چون کد تست ما اجرا میشه و هیچ اتفاقی نیفتاده. تازه ۲۵۰ میلی ثانیه بعد تابع updateHero صدا زده میشه.

فریم ورک Angular امکاناتی رو در اختیار ما گذاشته که بتونیم کدهای async رو تست کنیم:

  • به کمک تابع fakeAsync اعلام میکنیم که تستمون برای یک کد async هستش.
  • به کمک تابع tick اعلام میکنیم که تست به مدت زمانی که بهش پاس میدیم، باید منتظر بمونه و بعد ادامه پیدا کنه

با اجرای کد جدید میبینیم که تست مون پاس میشه.

تابع دیگه ای که میتونه کمک مون کنه تابع flush هست:

تابع flush صبر میکنه تا همه task های باقیمونده اجرا بشن، بعد تست رو ادامه میده.


اما برای شرایطی که منتظریم یک promise کارش تموم شه و همه حالت هاش اتفاق بیفته شرایط فرق میکنه.

وقتی یک promise در کد ما اجرا میشه، شرایط اون promise رو zonejs میدونه که تموم شده یا نه.

تابع fakeAsync هیچچی از zonejs نمیتونه. بنابراین ما باید تست مون رو در تابعی اجرا کنیم که از رویه zonejs اطلاع داره. ابزاری که فریم ورک Angular در اختیار ما قرار میده تابع async هستو

خیلی جاها ما از promise استفاده میکنیم. فرض کنید تابع save از یک promise استفاده میکرد:

ما هم تست مون رو با async و webStable پیاده سازی میکنیم:

تابع async شرایطی رو تست میکنه که داخل zonejs اتفاق می افته.


به آخر سری مقالات unit test در Angular رسیدیم. امیدوارم این سری مقالات براتون مفید باشه.

همه فایل ها و تست ها رو میتونید در github ببینید:

https://github.com/rohamtehrani/angular-unit-tests


برای مشاهده پست های بیشتر و ارتباط با من از طریق لینکدین اینجا کلیک کنید.

صفحه لینکدین من : https://www.linkedin.com/in/rohamtehrani



چند تا مقاله دیگه رو هم لینک میدم که اگر دوست داشتید مطالعه کنید:

کلاس های standalone در Angular

آشنایی با انگولار سیگنال ، فیچر جذاب نسخه ۱۶

آشنایی با کتابخانه RxJS

انواع Subject در RxJS

تشخیص آنلاین/آفلاین بودن اپلیکیشن در انگولار



موفق باشید :)


unit testانگولارangularآموزش انگولار
برنامه نویسی یک شغل نیست، یک هنره.
شاید از این پست‌ها خوشتان بیاید