چطور علمی و اصولی تست بنویسیم؟

تمامی اصولی که در این نوشتار آمده یک جور هایی بر اساس علم تست که در واقع سخت افزاری ها اون رو بسط و گسترش دادن نوشته شده. سخت افزاری ها از اون جایی که فرصت کمتری نسبت به نرم افزاری ها برای جبران اشتباهاتشون دارن، بهتر تست می کنند تا از عملکرد مدارات اطمینان حاصل کنن.

چرا اصلا تست بنویسیم

شاید برای شما که این مطلب رو مطالعه میکنید، کاملا جواب این سوال بدیهی باشه. هدف از تست نوشتن اطمینان پیدا کردن از عملکرد صحیح نرم افزار طراحی شده است وقتی که کاملا واقف هستیم که کد نوشته شده گسترش و یا تغییر پیدا خواهد کرد.

اگر قبلا نرم افزاری نوشته باشید که تست براش نوشته نشده حتما متوجه شدید که هر بار آپدیت کردن نرم افزار شما رو به استرس میندازه چون مشخص نیست که آیا کد جدید به خوبی کد قبلی کار میکنه یا خیر.

همچنین در صورتی که تست ننویسید اصولا از وجود یک سری ایرادات مطلع نمیشید. علتش هم این هست که معمولا وقتی به صورت دستی چیزی رو تست میکنیم فقط مسیر کارکرد اصلی رو بررسی میکنیم و مسیر های انحرافی و غیر سر راست بررسی نمیشن.

مشکل کجاست

اگر شروع به تست نوشتن کرده باشید کم کم این مساله که چقدر باید تست نوشت یا به چه شکلی نوشت و یا اصولا چه چیزی رو تست کرد براتون به صورت چالش در خواهد اومد. تست نویسی خود در واقع به همراه دانش دیگه ای معنی پیدا میکنه که اسم اون کد نویسی تست پذیر هست. به عبارت دیگه کد اصلی رو هم جوری بنویسیم که بشه اون رو تست کرد. برای اینکه به صورت پایه ای تر و اصولی تر این بحث رو دنبال کنیم ابتدا یک سری مفاهیم رو معرفی میکنیم:

ایراد یا فالت

یک ایراد یا فالت عبارت است از وجود یک مشکل در بخشی از کد که منجر به ایجاد مشکل در روند نرم افزار شود. به هنگام تست نویسی ما فالت های مختلف ایجاد میکنیم و مطمین می شویم که تاثیرات غیر قابل قبول در کد ایجاد نمی کنند.

معادل سازی فالت ها

وقتی دو فالت در عمل با یک تست شناسایی می شوند این فالت ها معادل هستند. مثلا فرض کنید ما با نوشتن یک تست هم ایراد موجود در جست و جو را پیدا میکنیم و هم همان تست میتواند ایراد موجود در مرتب سازی را به ما نمایش دهد. در این حالت همین یک تست برای هر دوی آنها کافیست و در واقع این دو فالت معادل هستند.

برتری فالت ها

فرض کنید با نوشتن تست برای یک فالت خاص ، وجود یک فالت دیگر پیدا شود. بنابراین ما نیازی به نوشتن تست جداگانه برای آن فالت نداریم و به اصطلاح فالت دوم بر فالت اول برتری دارد و خود به خود در نظر گرفته نمی شود.

کنترل پذیری

این واژه به معنای میزان توانایی ما به تغییر دادن یک حالت داخلی با استفاده از تغییر دادن ورودی است. یعنی این که چقدر برای ما سخت هست که وضعیت داخلی یک برنامه رو با عوض کردن ورودی عوض کنیم و در واقع تاثیر تغییر ورودی رو ببینیم.

این پارامتر در مهندسی تست در گرایش سخت افزار به صورت یک عدد برای مکان های مختلف موجود در یک مدار تعریف میشه. جاهایی که عدد بالاتری دارند یعنی برای تست آنها چالش بیشتری خواهیم داشت.

اما در مهندسی نرم افزار این عدد بیشتر یک عدد حسی و کیفی است. اگر منطقه ای از کد توسط ورودی های اولیه به راحتی عوض نمی شود یعنی آن نقطه از کد برای تست شدن ما را با چالش مواجه میکند.

مشاهده پذیری

این پارامتر نشون دهنده این هست که با دیدن یک نتیجه آیا میتوان ورودی ها و یا وضعیت داخلی پارامتر های موجود در کد را شناسایی کرد و یا خیر. در واقع هر چقدر اجزای مختلف کد مشاهده پذیری کمتری داشته باشند پیدا کردن ایرادات و یا باگ ها در آن سخت تر است.

استیت یا حالت

استیت یا حالت در واقع عبارت است از وضعیت فعلی نرم افزار. یعنی وضعیت تمام داده ها، متغیر های گلوبال و ...

اصولا تمام کد هایی که ما مینویسیم برای ایجاد یک حالت و سپس نشون دادن اون حالت به کاربر هست و نه چیز دیگه. بنابراین نحوه تغییر کردن این وضعیت در تست نویسی بسیار مهم هست.

بازتولید پذیری

این مفهوم در واقع بیان کننده این هست که آیا میتوان تست های مورد نظر را دقیقا با شرایط پیشین دوباره ایجاد کرد؟ در واقع آیا کد ما وابسته به پارامتر هایی خارج از منطق خود کد هست و یا خیر.

اسکوپ یا موضوعیت

مشخص میکند که این تست در چه لایه ای انجام میشود. مثلا آیا ما یک کارکرد خاص را تست میکنیم و یا یک سناریوی نوشته شده در نرم افزارمون رو تست میکنیم. مثلا در این رابطه تست های یونیت ( تست هایی که یک کارکرد خاص را تست میکنند ) و یا تست های اینتگریشن ( که یک سناریوی خاص را تست میکند) حایز اهمیت هستند.

حال که با این مفاهیم آشنا شدیم ابزار لازم برای شروع بحث را داریم. حال با نگاه کاربردی به سراغ مساله تست میرویم:

اصولا چه چیز را تست کنیم

این سوال مهمترین مساله در تست نویسی است. اگر در نظر دارید که برای همه چیز تست بنویسید باید به جرات بگویم که وقتتان را تلف میکنید! در واقع تست نویسی بیش از حد و تست مواردی که نیازی به تست ندارند فقط پروسه تولید نرم افزار را طولانی میکنند و نگه داری کد و تغییر آن را مشکل تر میکنن. پس بگذارید اول بررسی کنیم که چه چیزی را تست نکنیم!

جواب این سوال با دانستن فلسفه تست نوشتن به راحتی به دست می آید. ما تست مینویسیم که از کارکرد نرم افزار خود اطمینان داشته باشیم و بنابراین اگر تستی که مینویسیم کمکی به این امر نمی کند ننوشتن آن تست بهتر از نوشتن آن است. در واقع ما فقط چیز هایی را تست میکنیم که نیاز داریم از کارکرد آنها اطمینان حاصل کنیم. بنابراین اگر کدی ساده است و کارکرد خاصی ندارد نیازی به تست آن نیست.

بیایید با یک مثال شروع کنیم تا در قالب آن مثال موضوع را بهتر بررسی کنیم.

فرض کنید میخواهیم یک واسط برای گرفتن محصولات یک فروشگاه بنویسیم. مثلا فرض کنیم قرار است آدرس آن به این صورت باشد:

/api/products

این آدرس قرار است پاسخ های متفاوتی به کاربر عادی و ادمین فروشگاه بدهد. همچنین قرار است یک سری پارامتر از جمله فیلتر های مختلف و نحوه مرتب سازی را از کاربر بگیرد و محصولات مورد نظر را به صورت صفحه بندی شده نمایش دهد.

قبل از اینکه ادامه را بخوانید فکر کنید که اگر میخواستید این واسط را طراحی کنید و آنرا تست کنید چطور عمل میکردید.

این واسط چندین بخش مختلف دارد که آنها را به ترتیب مینویسیم:

  • بررسی اینکه چه کاربری دارد به این واسط درخواست میدهد.
  • بررسی اینکه آیا پارامتر های کاربر صحیح هستند یا خیر
  • تبدیل پارامتر های کاربر به پارامتر های سرچ و مرتب سازی برای دیتابیس با توجه به نوع کاربر
  • گرفتن نتایج از دیتابیس و تبدیل آنها به یک پیغام قابل دیدن برای کاربر
  • ارسال به کاربر.

حال فرض کنید این مراحل را نوشته ایم و در حال تست کردن کارکرد این واسط هستیم.

اولین نکته ای که حتما به آن توجه کرده اید تست کردن تمام سناریو های ممکن دریافت پاسخ توسط کاربر کاری بسیار زمان بر خواهد بود. بر فرض اینکه ما ۵ فیلتر مختلف، ۳ روش مرتب سازی مختلف، ۵ نوع کاربر مختلف و صحه بندی های مختلف داشته باشیم حداقل نیاز داریم که ۱۰۰ تست بنویسیم که تمام حالات را تست کنیم.

مشخص است که ما توان و وقت اینکار را نداریم و بنابراین نیاز است که تست ها را هوشمندانه بنویسیم. و همچنین کد را به چند بخش auth برای بررسی کاربر، validate برای بررسی ورودی ها، logic برای قسمت ایجاد فیلتر ها و نیز گرفتن نتایج از دیتابیس و بخش presentation تقسیم کنیم.

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

مثلا

۱-تست هایی فقط برای بررسی نوع کاربر و اضافه شدن فیلتر های مناسب آنها به لیست فیلتر ها

۲-تست هایی فقط برای بررسی ورودی کاربر

۳-تست هایی برای بررسی صحت عملکرد هر کدام از فیلتر ها به صورت جداگانه

۴-تست هایی برای بررسی عملکرد مرتب سازی

۵-تست های برای اطمینان از صحت خروجی

از مراحل بالا مرحله ۱ - ۴ به صورت تست های ساده unit میتوانند نوشته شوند و فقط نیاز به یک یا دو تست برای دادن ورودی به صورت کامل و گرفتن خروجی و اطمینان از صحت خروجی نیاز داریم.

در واقع برای بررسی منطق های پیچیده نیاز داریم تست را به ماژول های جداگانه تقسیم و جداگانه آنها را تست کنیم و سپس چند تست برای برای اطمینان از اتصال صحیح این ماژول ها داشته باشیم.

دقت کنیم که تست نوشتن به این صورت در صورتی که تجربه کافی داشته باشید بسیار سریع تر از امتحان ورودی ها به صورت دستی است! و در واقع زمان توسعه نرم افزار را به شدت کاهش میدهد.

در نوشتن تست های یونیت حتما مد نظر داشته باشید که این تست ها باید به صورتی باشند که به سادگی باز آفرینی شوند و نیز تاثیر جانبی ایجاد نکنند.

برای اینکار نیاز داریم کد را به صورتی بنویسیم که موجودیت هایی که قوانین بالا را به هم میزنند را به صورت خارجی به ماژول خود بدهیم ( در خود ماژول این موجودیت ها به صورت مستقیم استفاده نشوند). مثلا چیزهایی مانند گرفتن زمان سیستم نباید داخل خود ماژول مورد بررسی از سیستم گرفته شود. بلکه باید به صورت پارامتر به ماژول داده شود ( مثلا به صورت یک اینترفیس که این اینترفیس زمان را به ماژول میدهد) . اینکار باعث می شود که به هنگام تست ما یک پیاده سازی ساده و ثابت را به این ماژول بدهیم ( مثلا یک زمان ثابت) و عملکرد مختلف ماژول رو بر اساس آن تست کنیم. همچنین کد هایی که
ساید افکت ایجاد میکنند میتوانند به صورت فانکشن به ماژول اصلی داده شوند که باز هم به هنگام تست ما یک سری فانکشن بسیار ساده به جای آنها جایگزین کنیم که از صحت اجرا شدن آنها اطمینان حاصل کنیم.

به این روش به اصطلاح IOC یا inversion of control می گویند که علت این نام گذاری این است که کنترل این موارد به خارج از ماژول ما منتقل می شود.

دقت کنید که با اینکار می میتوانیم مطمین شویم که ماژول مورد بحث درست عمل میکند. در واقع اینکه عملکرد متدهایی که ساید افکت ایجاد میکنند و یا وضعیت غیر قابل پیش بینی ایجاد میکنند هیچ ربطی به ماژول فعلی ندارد و ماژول فعلی صرفا یک مصرف کننده این متد ها است و عملکرد این متد ها باید به صورت جداگانه در ماژول مربوط به خودشان تست شود.

نکاتی بسایر مهم در رابطه با تست نوشتن

به هنگام تست نوشتن و نیز طراحی خود کد اصلی باید مطمین شویم که میزان کنترل پذیری و مشاهده پذیری بالاست . اگر این عدد پایین است نشان می دهد که موضوعیت یا اسکوپ تست ما بیش از اندازه بزرگ است و لازم است که ماژولهای ما بیشتر و مناسب تر تقسیم بندی شود.

همچنین باید مطمین شویم که تست ها کاملا مستقل هستند و شرایط مورد نیاز تست قبل از اجرای آن فراهم می شود. مثلا در صورتیکه یک تست تکی بدون مشکل پاس می شود ولی به هنگامی که با بقیه تست ها اجرا می شود خطا می دهد، یعنی تست ها به یکدیگر وابستگی دارند.

به هنگام تست نوشتن نسبت به کد خود بی رحم باشید! تست ها نباید به آسانی سبز شوند! در غیر این صورت این تست ها ارزشی ندارند. وظیفه تست دادن چراغ سبز و خوشحال کردن شما نیست. شما باید تمامی حالاتی که کد شما ممکن است بد عمل کند را شناسایی کنید و درست نوشتن تست به این صورت به میزان زیادی تجربه نیاز دارد. ولی وسواس به خرج ندهید و در هر صورت مناسب ترین و بهینه ترین تست هایی را که میتوانید بنویسید. دقت کنید که فقط مسیر های معمولی را تست نکنید و تمام مسیر هایی که ممکن است کاربر طی کند را باید در نظر بگیرید.

پیاده سازی داخلی را تست نکنید! نحوه کارکرد داخلی یک ماژول ربطی به تست ندارد و وظیفه تست اطمینان از ورودی و خروجی های مختلف است.

فالت های معادل را با تست های یکسان شناسایی کنید و تست اضافی ننویسید. دقت کنید که نیازی نیست که دقیقا همه چیز را تست کنید. فقط تا جایی تست بنویسید که از عملکرد اطمینان حاصل کنید.

وقتی تست بنویسید که طراحی را تمام کرده اید. نوشتن تست وقتی که هنوز طراحی تمام نشده وقت تلف کردن است چون به همراه تغییرات زیاد باید تست ها هم عوض شوند.

سرویس های خارجی را تست نکنید اگر ماژول شما به منابع خارجی نیاز دارد آنها را تقلید کنید.

به هنگام تست خودتان بدانید چه چیز را تست می کنید و فقط یک چیز را تست کنید. یعنی وظیفه تست را از قبل مشخص کنید و اگر حس میکنید بیش از اندازه در تست خود چیز های مختلف را بررسی کرده اید دوباره به نحوه تست نوشتن خود فکر کنید و اگر نیاز است مسایل مختلف را جداگانه تست کنید.

تست های خودتان را دسته بندی کنید که بعدا بتوانید تعداد خاصی از آنها را اجرا کنید. با مرور زمان معمولا اجرا کردن تمام تست ها با هر بار تغییر کد زمان بر می شود و شما باید بتوانید یک تعداد تست خاص را جداگانه اجرا کنید.

دقت کنید که وظیفه برنامه شما ایجاد ساید افکت و تغییر استیت است! ولی وقتی کد مینویسید دقت کنید که هر ماژول فقط نسبت به عملکرد و وضعیت خود مسول باشد و نه بیشتر از آن. اگر چیزی از سیستم، شبکه، سرویس خارجی، دیتابیس و ... میگیرید یا قبل از تست آنها را آماده کنید ( مثل دیتابیس) و مطمین شوید آماده سازی اینها در صورتیکه کار ماژول شما صرفا استفاده از این موارد است توسط ماژول شما انجام نمیشود.

به عبارت دیگر از هارد کد کردن یک نوع پیاده سازی خاص بپرهیزید، سعی کنید ماژول شما از اینترفیس ها به جای پیاده سازی اصلی استفاده کند که به هنگام تست بتوانید آنها را با چیزهای ساده عوض کنید. همچنین در صورتیکه ماژول شما یک تاثیر خارجی را فراخوانی میکند این تاثیر را به صورت فانکشن به ماژول بدهید که باز هم به هنگام تست بتوانید آنرا عوض کنید. دقت کنید که در هر صورت این ماژول در کد اصلی همان وظایف قبلی را دارد ولی عملکرد آن میتواند به این صورت جداگانه تست شود.

کلام آخر

تست بنویسید ولی زیاد نه! اگر دستتون تو کدنویسی پر هست و تست نوشتن شمارو از حالت معمولی کندتر میکنه معنیش اینه که دوباره باید به تست نوشتنتون فکر کنید. منطق های پیچیده رو در ماژول های خودشون به صورت محلی تست کنید و تست های اینتگریشن فقط برای مطمین شدن از وصل شدن صحیح ماژول ها به هم استفاده شود.

اگر سوالی در این رابطه دارید میتونید تو توییتر از من بپرسید. خوشحال میشم پاسختون رو بدم. و اینکه نظراتتون برای من خیلی ارزشمنده. خوشحال میشم نظر بدید.