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

در بخش اول با کلیات unit test آشنا شدیم. انواع تست در Angular رو مرور کردیم. برای پایپ ها، سرویس ها و کامپوننت ها، تست isolated نوشتیم و اجرا کردیم.در گیت هاب میتونید نمونه کدهای هر دو بخش این مقاله رو مطالعه بفرمایید یا اپ رو نصب و اجرا کنید.بریم بخش دوم رو با نوشتن shallow test شروع کنیم:نوشتن shallow test برای کامپوننت هایادآوری کنم از بخش اول، که در shallow test میخوایم کلاس و تمپلت کامپوننت رو با هم تست کنیم. بدون اینکه درگیر وابستگی هایی مثل سرویس های استفاده شده بشیم.میخوایم برای کامپوننت Hero تست بنویسیم:به محتوای کلاس کامپوننت که نگاه کنیم، می بینیم یک Input و یک Output داره و همچنین یک تابع onDeleteClick که صدا زده میشه.یه نگاهی هم به تمپلتش بندازیم:یک دکمه داره که روی دکمه کلیک کنیم، تابع onDeleteClick صدا زده میشه.قدم اول: ساخت فایل تست ( در صورت نیاز ) و آماده کردن شرایط اولیهاول یک فایل میسازیم به اسم hero.component.shallow.spec.ts که تست هایی که قرار بنویسیم از تست های اتوماتیک از قبل ساخته شده (فایل hero.component.spec.ts ) کامپوننت مجزا بشه. در دنیای واقعی نیازی نیست بیشتر از یک فایل تست داشته باشیم. این فایل رو برای توضیح مقاله میسازیم.برای تست integration یک کامپوننت از TestBed استفاده میکنیم. ابزاری که شرایط تست همزمان کلاس و تمپلت کامپوننت رو برامون فراهم میکنه. بریم یه نگاهی به کد اولیه بندازیم:تابع configureTestingModuleتابع configureTestingModule یک آبجکت میگیره که کپی برابر اصل تعریف ماژول ها در Module Decorator هست. شامل بخش های declarations و providers و ...یه نگاهی به تعریف ماژول بندازیم:در واقع تابع configureTestingModule یک ماژول تستی برای ساختن کامپوننت و تمپلتش رو برای ما فراهم میکنه.متغیر fixtureمتغیر fixture در واقع یک wrapper روی کامپوننت هست که برای تست کامپوننت به کار میره و علاوه بر instance کامپوننت، یک سری امکانات بیشتر جهت تست در اختیارمون قرار میده.در واقع قبل از شروع هر تست، یک ماژول تستی و یک fixture از کامپوننت مون میسازیم. متغیر fixture یک property داره بنام componentInstance که کامپوننت ساخته شده رو در اختیار ما قرار میده.قدم دوم : نوشتن اولین shallow testموقع اجرای کد بالا به خطا میخوریم. ولی اول یه نگاهی به محتوای تست بندازیم:تابع detectChangesاین تابع در واقع ngOnInit رو صدا میکنه و صبر میکنه تا تمپلت و کامپوننت کاملا آماده بشن و تغییرات change detection تموم بشه.متغیر fixture.componentInstanceاین متغیر رو هم در قسمت قبل توضیح دادم. حاوی instance ساخته شده از کامپوننت جهت تست هست.حالا ببینیم خطایی که پیش اومده چی هست..چرا این خطا پیش اومده؟داخل تمپلت کامپوننت ما routerLink داریم و برای ساخت کامپوننت لازمش داریم. در اپ اصلی، اومدیم AppRoutingModule رو به ماژول app.module.ts اضافه کردیم که بتونیم از routerLink و سایر امکانات routing استفاده کنیم:اما در تعریف ماژول تستی، AppRoutingModule رو import نکردیم:برای import کردن امکانات routing جهت تست، Angular یک ماژول بنام RouterTestingModule در اختیار ما قرار داده که موقع تست بجای AppRoutingModule ازش استفاده کنیم.بنابر این تعریف ماژول تستی مون رو به شکل زیر عوض میکنیم:و با اجرای دوباره تست، میبینیم که کامپوننت درست ساخته میشه و تست ما هم پاس میشه.روش دوم حل مشکل RouterLinkیک روش دیگه برای اینکه بتونیم مشکل ناشناخته بودن RouterLink یا هر موجودیت دیگه ای داخل تمپلت رو ignore کنیم هست.میتونیم تعریف ماژول تستی رو با تغییر بخش schemas و به صورت زیر عوض کنیم:در واقع این تنظیم میگه هر مشکلی که در ساخت تمپلت پیش اومد رو نادیده بگیر و بهش گیر نده. من شخصا با این روش موافق نیستم. چون متوجه مشکلات تمپلت نمیشیم و میتونه خطاهایی از دست مون در بره. پس مراقب استفاده زیاد از این روش باشید.و به این ترتیب اولین shallow test به درد نخورمون رو نوشتیم و با فضای تست integration بیشتر آشنا شدیم.بررسی تمپلت render شدهحالا بریم یک تست جدی تر بنویسیم و تمپلت render شده رو بررسی کنیم.میخوایم ببینیم وقتی مقدار hero رو به کامپوننت پاس میدیم، اسمی که پاس دادیم داخل anchor هست یا خیر:تعریف detectChangesنکته اصلی این تست نحوه استفاده از تابع detectChanges هست. این تابع رو بعد از مقداردهی متغیر hero گذاشتیم . برای اینکه همه change ها اتفاق بیفته و کامپوننت به حالت stable برسه.متغیر fixture.nativeElementاین متغیر DOM Element کامپوننت رو در اختیار ما قرار میده و میتونیم مثل یک DOM Element واقعی تستش کنیم. برای همین میتونیم از querySelector استفاده کنیم.متغیر fixture.debugElementمتغیر مهم دیگه ای که fixture در اختیار ما قرار میده، debugElement هست. این متغیر شبیه nativeElement هست و در واقع یک wrapper روی debugElement هست و علاوه بر nativeElement، امکاناتی برای تست و هم در اختیارمون قرار میده. بریم تست بالا رو با debugElement بنویسیم:تابع queryتوابعی که امکان query زدن رو در debugElement در اختیارمون قرار میدن، با توابعی که در nativeElement استفاده میکنیم متفاوته و شامل query و queryAll و queryAllNodes هست.ابزار Byاین ابزار رو platform-browser فریم ورک Angular در اختیار ما قرار میده:دو امکان مهمی که در اختیار ما قرار میده عبارتند از By.css و By.directive .با تابع By.directive در ادامه آشنا میشیم. اینجا نحوه استفاده By.css رو می بینیم. و اینکه ورودیش علاوه بر نام tag میتونه شامل کلاس یا id تگ هم باشه.بریم سومین تست shallow رو با یک کامپوننت پیچیده تر بنویسیم که انواع dependency هم داره.من کامپوننت heroes.component.ts رو انتخاب کردم.قدم اول : ساختن heroes.component.shallow.spec.ts کنار کامپوننت heroesحالا که با شرایط تست آشنا شدیم بریم شرایط اولیه تست کامپوننت heroes رو بنویسیم:اما یک موضوعی هست. یک نگاهی به این خط از کامپوننت heroes بندازیم:این کامپوننت در constructor خودش یک instance از سرویس HeroService رو inject کرده. ما در اپ واقعی این سرویس رو در app.module.ts و داخل بخش providers تعریف کردیم. اینجا هم باید تعریفش کنیم وگرنه تست مون به خطا میخوره:میتونیم سرویس واقعی رو تعریف کنیم:ولی ترجیح میدیم که یک mock از سرویس HeroService رو تعریف کنیمو در بخش اول مقاله این کار رو کردیم:علاوه بر این در قسمت schemas ماژول تستی، NO_ERRORS_SCHEMA رو هم اضافه میکنیم:بریم اولین تست shallow رو بنویسیم و ببینیم وقتی heroes رو صد میکنیم، آیا تمپلت درست render میشه یا نه.یه نگاهی به کامپوننت heroes بندازیم:و یه نگاهی به تمپلت: به عنوان مثال اگر یک آرایه با سه آبجکت hero از سمت HeroService برگرده، ما باید سه المنت <li> داشته باشیم.بریم تستش رو بنویسیم:قدم اول : نمونه دیتا ( تکراری از بخش قبل )یک آرایه Heroes به تابع forEach اضافه میکنیم:داخل تابع beforeEach یک آرایه حاوی سه آبجکت Hero ساختیم.قدم دوم : نوشتن یک تابع تست ساده تریک تابع تست ساده تر به صورت زیر می نویسیم:و میبینیم که درست کار میکنه. اگر تست رو به شکل زیر تغییر بدیم و منتظر عدد ۴ باشیم، خطا میگیریم:حالا بریم سراغ اینکه تمپلت رو درست تست کنیم. اول از همه باید از دست NO_ERRORS_SCHEMA خلاص بشیم. چون هر خطایی در تمپلت اتفاق بیفته ما ازش بیخبریم.با برداشتن خط مربوط به تعریف schemas، با خطای زیر مواجه میشیم:در واقع کامپوننت app-hero که در تمپلت کامپوننت ما استفاده شده، در ماژول تستی تعریف نشده:می تونیم کامپوننت HeroComponent رو به ماژول تستی اضافه کنیم. ولی ما می خوایم یک Mock از روش بسازیم و به ماژول تستی اضافه کنیم. نمی خوایم کامپوننت HeroComponent روی تست کامپوننت ما تاثیر بگذاره.برای ساخت ماک از روی کامپوننت HeroComponent یک نگاهی به کدش میندازیم:قبل از ساخت mock به نکات زیر توجه کنیم:ما نیازی به تمپلت واقعی HeroComponent نداریم و یک تمپلت ساده الکی برامون کافیه.نیازی به Output کامپوننت HeroComponent نداریم، فقط میخوایم المنتش تعریف شده باشه.اسم کلاسش هم مهم نیست. ما میخوایم app-hero رو تعریف کنیم.به توابع داخلی HeroComponent هم نیازی نداریم. نمای کلی کلاسش رو لازم داریم.بنابر این یک کلاس به شکل زیر میسازیم:دقت کنید که export هم حذف شده و نام کلاس به FakeHeroComponent تغییر کرده.حالا باید این کامپوننت fake رو داخل مازول تستی بجای HeroComponent تعریف کنیم:و دیگه خطایی بابت تمپلت نمیگیریمبنابر این با برداشتن NO_ERRORS_SCHEMA و حل مشکلات ساخته شدن ماژول تستی، دو کار مهم کردیم:حساسیت تست ها رو به render شدن تمپلت ها حفظ کردیمکامپوننت و تمپلت رو بدون وابستگی به بخش های دیگه کد برای تست آماده کردیموقتشه بریم آخرین تست shallow روی کامپوننت HeroesCompoent . میخوایم ببینیم وقتی یه آرایه از چند تا hero رو به متغیر heroes پاس میدیم، آیا به همون تعداد <li> ساخته میشه یا خیر.قبلا همه توضیحات رو دادیم و با کل این تست آشناییم. نیازی به توضیح اضافه نداره.اگر مقدار سه رو به یک عدد دیگه تغییر بدیم تست مون fail میشه و یعنی تست مون درسته.بحث shallow test ها تموم شد.در بخش سوم مقاله مروری خواهیم کرد به deep integration تست ها.برای مشاهده پست های بیشتر و ارتباط با من از طریق لینکدین اینجا کلیک کنید.صفحه لینکدین من : https://www.linkedin.com/in/rohamtehraniچند تا مقاله دیگه رو هم لینک میدم که اگر دوست داشتید مطالعه کنید:کلاس های standalone در Angularآشنایی با انگولار سیگنال ، فیچر جذاب نسخه ۱۶آشنایی با کتابخانه RxJSانواع Subject در RxJSتشخیص آنلاین/آفلاین بودن اپلیکیشن در انگولارموفق باشید :)