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

در این سری مقالات میخوایم بااصول unit test و نحوه پیاده سازیش در Angular آشنا بشیم. بخش هایی که در این سری مقالات بررسی میکنیم شامل موارد زیر میشه:بخش اول:با مفاهیم unit test و تست های Angular آشنا میشیم.با ابزار انگولار برای تست آشنا میشیم.تست های isolated رو برای کامپوننت ها، سرویس ها و پایپ ها پیاده سازی میکنیم.بخش دوم: تست های shallow integration رو پیاده سازی میکنیم.بخش سوم: تست های deep integration رو پیاده سازی میکنیم.بخش چهارم:تست اجزای تمپلت مثل input text ها رو پیاده سازی میکنیم.با تست کدهای asyncronous آشنا میشیم.سه نکته:۱. در این مقاله در مورد اینکه اول تست رو بنویسیم بعد کد رو پیاده سازی کنیم یا برعکس، صحبت نمیکنیم. در مورد تست های end to end و ابزارهاشون هم صحبتی نمیکنیم.۲. نیازمندی های مطالعه این مقاله آشنایی کافی با جاوااسکریپت و فریم ورک انگولار هست.۳.اگر پروژه ای دارید که از توسعه ش مدتی گذشته و بزرگ شده و میخواید شروع کنید براش تست بنویسید، با اولین قدم که اجرای تست هاست، ممکنه به انبوهی از خطاها برسید. لطفا حتما بیخیال نشید!! پیشنهاد میکنم تمرینات این مقاله و مقاله بعدی رو کامل بخوانید و با یک اپلیکیشن تازه و خالی تمرین کنید. به موضوع تسلط نسبی پیدا کنید و بعد خطاهای پروژه اصلی رو یکی یکی رفع کنید.فایل تست های نوشته شده رو میتونید در گیت هاب ببینید. یا پروژه رو دانلود و نصب کنید.لینک پروژه این سری مقالات: https://github.com/rohamtehrani/angular-unit-testsهدف unit testing به تجربه مناین لیست صرفا خلاصه مطالعات و تجربیات من از نوشتن unit test هست:انتظارمون از رفتار یک بخش کد رو به طور اتوماتیک بررسی میکنیم.به نوعی انتظارمون از رفتار یک بخش کد رو داکیومنت میکنیم. وقتی همکارمون کد تست ما رو بخونه متوجه میشه این کد قراره چه کاری انجام بدهسرعت آمادگی همکارهای جدید برای onboard شدن روی توسعه اپ بالا میره.واقعا هزینه نگهداری و توسعه کد در طولانی مدت کاهش پیدا میکنه.اگر باگ هایی که بعد از نوشتن کد تولید شدند رو رفع کنیم و برای دوباره تکرار نشدنشون کد تست بنویسیم، مجموعه باارزشی از اطلاعات رو برای خودمون و نفرات بعدی ذخیره میکنیم.مجموعه اهداف بالا یعنی کد باارزش تری رو توسعه میدیم.مفهوم Mocking ( تقلید کردن )یکی از مهم ترین مفاهیم unit test مفهوم mocking هست. هدف ایزوله کردن بخشی از کده که داریم تست میگیریم.خیلی از کلاس ها و component ها ممکنه یک یا چند dependency داشته باشند. ممکنه اون بخش از کد که میخوایم تست بگیریم وابسته به یک dependency باشه. ما نمیخوایم عملکرد این dependency (ها) روی تست ما اثر بگذاره.اینجاست که مفهوم mocking مطرح میشه. در واقع بجای آبجکت اصلی dependency که کد ما بهش وابسته ست، کلاس یا سرویس مشابهی میشینه و ادای کد اصلی رو در میاره. در واقع عملکرد مورد نظر ما رو برای تست پیاده میکنه.این کار به ما اجازه میده که در یک تست، فقط کد مدنظر مون رو تست بگیریم و عملکرد درست یا غلط dependency ها رو حذف کنیم.چهار نوع Mock داریم: مدل Dummiesمدل Stubsمدل Spiesمدل True Mocksمدل Dummies:فقط جای آبجکت اصلی میشینه و property ها و متدهای مشابه و لازم برای تست رو شبیه سازی میکنه. هیچ حرکت دیگه ای انجام نمیده.مدل Stubs:آبجکتی که کارش بررسی صدا زدن ها (calls) و نتیجه ها (results) ست.مدل Spies:به ما اطلاع میده که چه متدی صدا شده یا چند بار صدا شده یا چه پارامترهایی برای صدا زدنش استفاده شده.مدل True Mocksرفتاری را در dependency پیاده سازی میکنه که ما موقع تست کدمون انتظار داریم. صرف نظر از اینکه رفتار واقعی dependency درسته یا نه.لازمه دو تا نکته رو در نظر داشته باشیم: ممکنه در مقاله ها یا کتاب های دیگه اسامی دیگه ای استفاده کنن.خیلی موقع ها مرز بین Stub و Spies درست مشخص نیست.مدل های unit test در Angularدر Angular دو مدل unit test داریم:تست isolatedتست intergrationتست isolatedدر این مدل تست، به طور خاص یک کامپوننت، سرویس یا پایپ رو دستی میسازیم و پارامترهای مورد نظرمون رو بهش پاس میدیم و رفتارش رو بررسی میکنیم.تست integrationدر این مدل تست، ما یک ماژول رو دستی میسازیم . کامپوننت مون رو داخلش اجرا میکنیم . کامپوننت و تمپلتش رو بررسی میکنیم.تست integration رو به دو صورت انجام میدیممدل Shadow: یک کامپوننت رو به تنهایی بررسی میکنیممدل Deep: یک کامپوننت رو به همراه کامپوننت های child ش بررسی میکنیم.آشنایی با ابزارهای Angular برای unit testفریم ورک Angular پس از نصب شدن، دو ابزار رو برای نوشتن unit test در اختیارمون قرار میده. ابزار karmaکتابخانه Jasmineکتابخانه Karma یک test runner هست که تست ها رو روی مرورگر اجرا میکنه.کتابخانه Jasmine ابزارهایی برای ساخت mock ها و بررسی رفتار کد رو در اختیارمون قرار میدهابزارهای دیگه ای هم هستند که برای نوشتن unit test ها در Angular میتونند استفاده بشن:JestMocha/ChaiSinonTestDoubleWallabyلیست شون رو نوشتم که اگر دوست داشتید بهشون سر بزنین و در موردشون مطالعه کنین.اولین نمونه کد تستداخل پوشه /src/app یک فایل جدید میسازیم به اسم first-test.spec.ts .به صورت پیش فرض، هنگام اجرای تست ها، همه فایل هایی که اسمشون به .spec.ts ختم میشه، فایل تست در نظر گرفته میشن اول کد رو ببینیم و بعد بریم سراغ توضیحشدر ادامه هر قسمت رو توضیح میدم. اگر لازم بود، لطفا برگردید و کد رو مرور کنید.تابع describeیک تابع از کتابخانه Jasmine که به ما اجازه میده یک گروه از تست ها رو بنویسیم.این تابع دو تا ورودی داره. پارامتر اول یک متن توضیح درباره گروه تسته . پارامتر دوم یک تابع callback هست که داخلش کد تست ها رو می نویسیم کلمه sutمخفف System Under Test هست. شامل اون بخشی از کده که میخوایم بزاریمش زیر تست اتوماتیک. البته sut صرفا اسم متغیره. من معمولا از comp برای کامپوننت زیر تست، از service برای سرویس زیر تست و از pipe برای پایپ زیر تست استفاده میکنم.تابع beforeEachتست های اتوماتیک پشت سر هم اجرا میشن. یکی از اصولی که باید برای نوشتن تست در نظر بگیریم، اینه که هیچ تستی در نتیجه تست های دیگه مداخله نکنه. برای رسیدن به این هدف باید هر تست رو در محیط ایزوله شده خودش انجام بدیم.وظیفه تابع beforeEach اینه که قبل از اجرای هر تست این گروه، اجرا بشه و یک محیط ایزوله رو برای تست پیش رو فراهم کنه. در واقع هر تست رو به وضعیت اولیه برمیگردونهتابع itهر تست رو داخل یک تابع it مینویسیم. این تابع شامل دو پارامتر ورودیه. پارامتر اول توضیح مربوط به اون تست و پارامتر دوم یک تابع callback هست که داخلش کد تست مون رو مینویسیم.بهتره طوری توضیح رو بنویسیم که وقتی توضیح describe رو میخوانیم و بعدش توضیح it رو میخوانیم، انگار داریم یک جمله کامل از تست رو میخوانیم:مثلا داریم یک گروه تست برای OrderService مینویسیم و میخوایم تابع getOrder رو تست کنیم:Order Service getOrder should retrieve the correct orderیا مثلا این طوری هم میتونیم دسته بندی کنیم:الگوی AAAهر unit test شامل یک الگوی سه مرحله ایه که همشون با A شروع میشن:مرحله اول: Arrangeمرحله دوم: Actمرحله سوم: Assertدر مرحله Arrange همه شرایط لازم برای اجرای تست مون رو فراهم میکنیمدر مرحله Act کد هدف رو اجرا میکنیم و در واقع زیر تست میبریمشدر مرحله Assert بررسی میکنیم ببینیم به نتیجه مورد نظر رسیدیم یا نه ( وضعیت جدید درسته یا خیر‌ )مثال اول یک مثال ساده بود. در مثال های بعدی عمیق تر با این الگو آشنا میشیم.مفاهیم DAMP و DRYمفهوم DRY یا Don't Repeat Yourself رو باهاش آشنایی داریم. یعنی خودت رو تکرار نکن. در واقع کد تکراری داخل برنامه ننویسیم.مفهوم DAMP مخفف Descriptive And Meaningful Phrases هست. یعنی کد تست ما روایتگر رفتار کد ماست. در واقع موقع نوشتن تست، داریم یک داستان کامل از رفتار کد رو به صورت قابل فهم توصیف کنیم. پس بهتره کدهای تست قابل فهم و واضح نوشته بشن.در واقع اگر تکرار برخی از قسمت های تست به روایت ما از کد کمک کنه، میتونیم هر چقدر که لازمه تکرارش کنیم. این قانون نیست بلکه ترجیحه.تکنیک هایی هست که به پیاده سازی مفهوم DAMP کمک میکنه:تابع beforeEach رو با حداقل کد لازم بنویسیم و شلوغش نکنیم.تنظیمات خیلی مهم و critical رو داخل تابع it بنویسیم.الگوی AAA رو داخل هر تابع it تا حد ممکن رعایت کنیم.اجرای unit test های Angular توسط angular cliاگر فایل package.json رو باز کنیم، در بخش scripts یک لیست از اسکریپت ها رو میبینیم:اسکریپتی که الان باهاش کار داریم، test هست. یعنی میتونی به دو صورت اجراش کنیم:که در واقع دستور ng test رو اجرا میکنه . یا مستقیما دستور تست رو اجرا کنیم:بعد از اجرای دستور تست، یک مرورگر باز میشه و شما میتونید فضای کلی karma رو ببینید که تست ها داخلش اجرا شدند:انتهای command line هم می تونیم خلاصه ای از تست های اجرا شده رو ببینیم.نوشتن isolated test پایپ ها در Angularبریم سراغ نوشتن تست برای پایپ ها که ساده ترین قسمت نوشتن تست در Angular هست. پایپ ها در واقع یک کلاس هستند که تابع transform دارند. این تابع یک ورودی میگیره و خروجی تحویل میده.فرض کنید واحد پولی که اپ بر اساسش کار میکنه ریاله. ولی ما میخوایم به کاربر تومن نمایش بدیم. پس یک pipe داریم که قیمت ها رو به ریال میگیره و به تومن برمیگردونه:اول میخوایم مطمئن بشیم پایپ به درستی ساخته میشه. بعد میخوایم مطمئن بشیم اگر عدد ۱۰۰۰ رو بهش بدیم، عدد ۱۰۰ برمیگردونه. به توابع it اولیه نگاه کنید. مراحل الگوی AAA رو هم نوشتم:حالا به تست های زیر دقت کنید.نکته اول:قسمت Arrange از تست ها برداشته شده و داخل beforeEach نوشته شده. در واقع ساخت آبجکت پایپ هم Arrange حساب میشه هم ایزوله کردن محیط تست هست. درباره این موضوع که ساخت آبجکت کجا اتفاق بیفته ، بین علما اختلافه :)) درواقع الگوی AAA داخل تابع it رعایت نشده.میتونیم سفت و سخت بگیریم یا نگیریم. به نظر من مهم رفتار کده که درست پوشش داده بشه و اینجا درست پوشش داده شده.نکته دوم:به داستانی دقت کنیم که تست ها از TomanPipe برامون روایت میکنن: پایپ toman مقادیر عدد و string میگیره و یک دهم مقدارش رو به عنوان عدد برمیگردونه.اگر عدد ضریب ده نبود، عدد رو به پایین رند میکنه.با عدد صفر مشکلی نداره و صفر برمیگردونه.با استرینگ "0" مشکلی نداره و عدد صفر برمیگردونه.هر تغییری که به رفتار پایپ بدیم، روایتی ازش باید در کدهای تست باشه.مثلا قراره به ازای مقدار null عدد صفر رو برگردونه. پس تست زیر رو هم اضافه میکنیم:حالا باید کد پایپ رو طوری عوض کنیم که نتیجه مورد نظر ما رو به درستی برگردونه:کد جدید پایپ تغییر کرده و تست بالا رو پاس میکنه. اگر میخوایم تعریف دقیق تری از رفتار پایپ داشته باشیم باید به فایل تست ، داستان های بیشتری اضافه کنیم.تست ها همیشه بخشی از رفتار کد رو پوشش میدن (بین صفر ! تا صد !). وقتی باگی پیدا میشه یعنی بخش مهمی از رفتار کد، توسط تست ها پوشش داده نشده. بنابراین الزاما پوشش ۱۰۰٪ رفتار کد بهترین حالت نیست. هدف نهایی از نوشتن تست کد جلوگیری از باگ های احتمالی و تکرار باگ های گذشته ست.میزان پوشش رفتار کد (٪) هدف اصلی نوشتن تست نیست.نوشتن isolated test برای سرویس هابا توجه به قسمت های قبل، دیگه تو نوشتن کدهای تست مشکلی نداریم. فقط باید ببینیم سرویس رو چطوری تست کنیم. فرض کنید یک سرویس ساده به اسم MessageService داریم در فایل message.service.ts :میخوایموضعیت اولیه سرویس رو بررسی کنیمببینیم به ازای صدا کردن تابع add درست کار میکنهببینیم به ازای صدا کردن تابع clear همه مسیج ها پاک میشهببینید چطور کد تست زیر در فایل message.service.spec.ts با روایت های بالا همخوانی داره.نوشتن isolated test برای کامپوننت ها: فرض کنید برای کامپوننت Heroes میخوایم تست بنویسیم در فایل heroes.component.ts:این کامپوننت یک لیست بنام heroes داره و سه تابع بنام های getHeros، add و deleteشروع میکنیم به نوشتن فایل تست heroes.component.spec.ts :همون طور که می بینیم، در قسمت constructor کلاس کامپوننت، ما سرویس Hero Service رو inject کردیم به کلاس. بنابراین برای ساخت کامپوننت بهش نیاز داریم. موضوع اینه که نمیخوایم از سرویس واقعی Hero Service استفاده کنیم. چون ممکنه باگ های اون سرویس روی نحوه کار کامپوننت ما تاثیر بگذاره . ما میخوایم کامپوننت رو در محیط ایزوله ای تست کنیم و از رفتارش مطمئن بشیم.پس اینجا نیاز داریم یک Mock از سرویس Hero Service بسازیم. بریم یک نگاه کنیم ببینیم سرویس Hero Service در کامپوننت ما چطوری استفاده شده؟همون طور که در مثال زیر مشخصه ، سه تابع این سرویس در کامپوننت استفاده شده:بنابر این به کمک ابزار jasmine.createSpyObj یک آبجکت از heroService میسازیمو به کامپوننت پاس میدیم:در قدم بعدی میریم روی تابع delete تست مینویسیم ببینیم درست کار میکنه یا نه :بعد از یک مدت دیگه نیاز نیست مراحل رو با کامنت توضیح بدیم. ولی فعلا برای مرور مراحل الگوی AAA کامنت میگذارم.خب حالا وقتی تست رو اجرا میکنیم به مشکل میخوریم:خطا میگه که تست موقع صدا زدن subscribe به مشکل خورده. وقتی برگردیم و کامپوننت رو نگاه کنیم، می بینیم که داخل تابع delete کامپوننت، به صورت زیر تعریف شده:در واقع سرویس heroService تابع deleteHero رو صدا کرده و بعد subscribe رو صدا کرده. درحالیکه در mock object ما امکانی برای subscribe وجود نداره. پس باید یک کم mock object رو تغییر بدیم. آماده کردن mock object در فضای Arrange تست انجام میشه:در واقع در کد بالا، تابع deleteHero رو طوری تغییر دادیم که وقتی صدا زده میشه، یک observable برگردونه که بشه داخلش subscribe کرد.در صورت نیاز به آشنایی با کتابخانه RxJS و توابعش مثل of یا subscribe میتونید این مقاله رو مطالعه کنید.با تغییر mock object تست ما آماده اجراست و با موفقیت کار میکنه.حالا میخوایم بررسی کنیم و ببینیم وقتی که تابع delete کامپوننت رو صدا میزنیم، حتما تابع deleteHero از سرویس Hero Service صدا زده میشه.برای این کار یک تست دیگه به فایل تست مون اضافه میکنیم:برای این کار، کتابخانه jasmine تابع toHaveBeenCalled رو در اختیارمون قرار داده.اگر به کد کامپوننت بریم و خطی رو که تابع deleteHero صدا زده شده رو کامنت کنیم، تست مون به خطا میخوره و این یعنی درسته:می بینیم که تست مون به خطا میخوره:تست درست، شرایط درست رو پاس میکنه و شرایط غلط رو پاس نمیکنه. قرار نیست تست ما صرفا کدی رو که نوشتیم تایید کنه.برای تست نحوه صدا کردن deleteHero، شاید اینکه فقط صدا زده میشه یا نه مد نظرمون نباشه. شاید بخوایم ببینیم تابع با پارامتر درستی صدا زده میشه یا نه. برای این تست از تابع toHaveBeenCalledWith استفاده میکنیم:همینطور ممکنه بخوایم ببینیم تابع deleteHero فقط یک بار صدا زده میشه. دوبار یا بیشتر صدا زده نمیشه:یک نکته از ‌angular cliبرای خلاص شدن از دست sourcemap ها میتونید با اضافه کردن یک flag بنام source-map-- به دستور ng test اون ها رو حذف کنید:تمرکز روی یک تستقبل از اینکه بریم سراغ تست های integration یه نکته خیلی مفید رو مرور کنیم. وقتی نرم افزار بزرگ و بزرگتر میشه تعداد تست ها بیشتر و بیشتر میشه. قابل تصوره که در یک نرم افزار توسعه یافته، با اجرای ng test حجم زیادی از تست ها با هم اجرا بشن.فرض کنیم که الان میخوایم یک کامپوننت جدید بنویسیم و میخوایم تا انتهای توسعه کامپوننت فقط سناریوهای همین کامپوننت رو تست کنیم. کاری با بقیه سیستم نداریم. چه کار کنیم؟دو راه برای این موضوع هست.راه حل اول : استفاده از fdescribe و fitوقتی پیشوند f ( مخفف force ) رو به به توابع describe و it بچسبونیم، test runner همه سناریوهای موجود در نرم افزار رو جکع آوری میکنه. ولی فقط اونهایی رو اجرا میکنه که در fdescribe و fit تعریف شدند. بقیه سناریوها رو در حد نمایش و به صورت disable نمایش میده.بیایم این وضعیت رو روی تست Heroes Component اجرا کنیم و تست اولش رو از it به fit تغییر بدیم:نتیجه اجرای تست هامون به صورت زیر میشه:همه تست ها شامل ( TomanPipe و Heroes Component ) جمع آوری شدند ولی فقط سناریوی delete hero اجرا شد.راه حل دوم: استفاده از پارامتر include هنگام اجرای ng testدر این حالت فقط فایل هایی تست میشن که شامل تعریف مسیر میشوند.میتونیم مستقیم آدرس یک فایل تست رو به پارامتر include بدیم و فقط روی همون فایل تست بگیریم.میتونیم بیش از یک بار از پارامتر include استفاده کنیم.در راه حل اولمجموعه فایل های تستی رو محدود میکنیم.سرعت اجرای تست به شدت بالا میرهولی در راه حل دومسناریوهای اجرایی رو محدود میکنیم.سرعت ست آپ تست کمه ولی سرعت اجرای تست ها بالاست.در مقاله بعدی ادامه بحث تست Angular رو با intergration test پیش میبریم.برای مشاهده پست های بیشتر و ارتباط با من از طریق لینکدین اینجا کلیک کنید.صفحه لینکدین من : https://www.linkedin.com/in/rohamtehraniچند تا مقاله دیگه رو هم لینک میدم که اگر دوست داشتید مطالعه کنید:کلاس های standalone در Angularآشنایی با انگولار سیگنال ، فیچر جذاب نسخه ۱۶آشنایی با کتابخانه RxJSانواع Subject در RxJSتشخیص آنلاین/آفلاین بودن اپلیکیشن در انگولارموفق باشید:)