در این سری مقالات درباره تست های فریم ورک 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 در کامپوننت اصلی صدا زده میشه یا خیر:
تست با موفقیت پاس میشه
حالا اگر انتظار تست مون رو عوض کنیم و عمدا پارامتر اشتباهی رو تست کنیم، می بینیم که تست اشتباه مون رو متوجه میشه و 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
حالا بریم ببینیم در کلاس کامپوننت چه خبره:
بریم تستش رو بنویسیم و ببینیم نکته های بالا رو چطوری پیاده سازی کردیم:
بخش Assert تست رو به روش های دیگه هم میشه پیاده سازی کرد که دقیق تر هم باشه. صرفا برای این اینطوری نوشتم که کد متفاوتی رو ببینیم.
بیایم نگاه دوباره ای روی mock کردن سرویس های inject شده به کامپوننت بندازیم.
کلاس کامپوننت hero-detail.component.ts رو در نظر بگیرید:
میخوایم آبجکت 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 رو تست کنیم:
با اجرای کد جدید میبینیم که تست مون پاس میشه.
تابع دیگه ای که میتونه کمک مون کنه تابع 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
آشنایی با انگولار سیگنال ، فیچر جذاب نسخه ۱۶
تشخیص آنلاین/آفلاین بودن اپلیکیشن در انگولار
موفق باشید :)