چگونه برای پروژه های توسعه یافته تست بنویسیم؟

خب قرار هستش در خصوص موضوع جذاب تست نویسی برای پروژه های توسعه یافته(لاراولی) از تجربیات خودم براتون بگم! پس اماده باشید و یه لیوان قهوه ام بزارید کنارتون که یه وقت وسط خوندن این مطالب خوابتون نبره!!

بر اساس آمار سایت confidentlaravel از 15000 پروژه ای که با پکیچ shift به ورژنای بالاتر ارتقا پیدا کردن تنها 17% از اونها تست داشتن و خب این یعنی یک فاجعه!!
از اکثر برنامه نویسان(سطح متوسط رو به بالا) اگر بپرسید چرا پروژتون تست نداره میگن فرصت لازم برای تست نوشتن نداریم!! از نصف نصفشون میشه این حرفو قبول کرد اما ¾ مابقی نمیدونن دقیقا از کجا باید شروع به نوشتن تست کنند!(البته بنظر من)

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

سوال : چرا ما برای پروژه هامون تست مینویسیم؟
جواب : برای اینکه از صحت عمکلرد "بخشای مختلف" برناممون مطمئن بشیم.

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

ما اگر یک پروژه لاراولی رو در نظر بگیریم خب این پروژه از یکسری بخش های ثابت که بیسش توسط لاراول نوشته شده مثل : model – controller– event – listener - middleware – validation– route –و ... تشکیل شده!

علاوه بر سرویس ها و ماژول هایی که ما خودمون از بیس طراحیشون میکنیم از اکثر بخش های بالا(یا بعضا تمامشون) هم ممکنه تو پروژه های لاراولیمون استفاده کنیم.

خب اینجا مهم ترین سوال برای کسی که اولین بار میخواد برای یک پروژه تست بنویسه این باشه که از کجا شروع کنم؟ چه تستی بنویسم؟

یه آدم خفنی به اسم Jason McCreary تو این زمینه میگه اول ساده ترین قسمت پروژتون رو انتخاب کنید بعد هر نوع تستی که بلد هستین رو براش پیاده کنید! فرق نمیکنه unit test باشه یا feature test (پایین تر فرق این دوتارو توضیح میدم).

حالا بنظرتون دلیل این امر چیه؟

آفرین! برای اینه که در انجام هر کاری بایستی اول اعتماد بنفس لازمش رو داشته باشیم، بعد که اعتماد بنفسش رو پیدا کردیم میتونیم خیلی راحتتر اون کار رو انجام بدیم.

در ادامه همین آدم خفن میگه از Http test ها شروع کنیم، چرا؟؟

چون با Http request ها خیلی از بخش های پروژمون درگیر میشه و با استفاده از ارسال یک درخواست میتونیم صحت خیلی ازاین بخش هارو بررسی کنیم. از اینرو برای شروع میایم انواع درخواست ها رو با توجه به مسیرهایی که برای برناممون در نظر گرفتیم به سرور ارسال میکنیم و بخش های مختلف پروژمون رو باهاش تست میکنیم. در بعضی از درخواست ها نیاز هستش که یکسری داده هم همراه باهاشون به سمت سرور ارسال بشه، مثلا برای ثبت نام یه کاربر باید اطلاعاتش رو خودمون به صورت دستی پر کنیم و ارسال کنیم. روش دیگه ای که وجود داره استفاده از کتابخانه faker هست که به صورت پیشفرض این کتابخانه بر روی لاراول وجود داره. فکر میکنم از روی اسمش بتونید حدس بزنید که ساز و کار این کتابخانه چی هستش!

ا Faker یک کتابخانه جهت تولید داده های مصنوعی برای انواع مختلف داده ها هستش.

لزوم استفاده از این کتابخانه بیشتر در model factory ها احساس میشه. حالا model factory چیه؟؟

اجازه بدین با یک مثال توضیح بدم...

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

به دلیل تمیز نبودن روش بالا و همچنین مشکلاتی که برای ما به وجود خواهد آورد لاراول سیستمی رو تحت عنوان model factory از لاراول 5.1به بعد معرفی کرد model factory . ها جهت ایجاد داده های مصنوعی برای مدل ها به جهت استفاده در testingو database seeding به وجود اومدن.

برای توضیحات بیشتر پیشنهاد میکنم حتما داکیومنت خود لاراول رو مطالعه کنید.

خب همون طور که میدونید لاراول یه دستوری داره با عنوان php artisan make:test“ className” که این دستور برای شما یک کلاس تست با نام وارد شده (className) در داخل دایرکتوری tests/Feature ایجاد میکنه. از اونجایی که میدونم تفاوت Unit test با Featuer Test رو میدونید اما من باز هم یه توضیح مختصری در این مورد میدم.

در unit test ما از دید برنامه نویس به قسمت های مختلف پروژه نگاه میکنیم و صحت یه متد یا قسمتی از یک کلاس رو مورد بررسی قرار میدیم.

اما در feature test از دید کاربر نهایی به سیستم نگاه میشه و اطمینان حاصل می­کنیم که سیستم همانطور که کاربران از آن انتظار دارند کار می­کند. در این نوع از تست ها ممکنه بخش های مختلفی از پروژه مثل Database و... درگیر شوند.

بزارید با یک مثال براتون توضیح بدم، کلاس مربوط به ثبت نام یک کاربر رو در نظر بگیرید.
این کلاس از یک تابع به اسم register تشکیل شده که بدین شکل هستش :

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

اما در unit test میتونیم صحت اعتبار سنجی داده های ارسالیمون رو بررسی کنیم که ببینیم آیا این اعتبار سنجی به درستی عمل میکند یا خیر.در واقع در unit test صحت ساز و کار بخشی از یک روند کامل رو بررسی می­کنیم.

خب حالا با توجه به این توضیحات Http test ها جزو کدوم دسته محسوب میشن؟؟

نکته ای که در خصوص نحوه مسیر دهی به کلاس های تست بهتره که رعایت کنیم(best practice) اینه که مثلا اگر قرار هستش برای یک کنترلر که در آدرس Http/Controller/Auth/LoginController قراره داره تستی نوشته بشه، همین آدرس رو برای کلاس تستش هم در نظر بگیریم که تست هامون تفکیک شده باشه و همچنین تشخیص این موضوع که برای چه بخش هایی از پروژه تست نوشته شده راحتتر باشه.

خب دیگه توضیح دادن کافیه بریم سراغ یه مثال ساده.

مثال : تستی رو می­نویسیم که صحت روند ثبت نام یک کاربر رو بررسی میکنه.

بعد از نوشتن تست مورد نظرمون باید اون رو اجرا کنیم.

با استفاده از دستور ./vendor/bin/phpunit میتونید تمامی تست های نوشته شدتون رو اجرا کنید.

در این دستور کلمه ای که نیاز به توضیح داره phpunit هستش که من بسنده میکنم به توضیح خود سایت phpunit که امیدوارم انگلیسیتون خوب باشه و متوجه تعریفش بشین.

PHPUnit is a programmer-oriented testing framework for PHP.It is an instance of the xUnit architecture for unit testing frameworks.

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

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

در خط 25 درخواستی رو با متد POSTبه آدرس مربوطش به همراه اطلاعات کاربرمون ارسال کردیم.

این درخواست بعد از طی کردن یه مسیری جوابی رو به ما برمیگردونه که میتونه هر چیزی باشه.

در خط 26، status code مورد انتظارمون رو بررسی کردیم که بایستی این statusبا مقدار وارد شده یکی باشد.

چیزی که ما از ارسال این درخواست انتظار داریم این هستش که در صورت درست بودن اطلاعات ارسالی، کاربری با این اطلاعات در جدول user وجود داشته باشه که در خط 28 این مورد رو بررسی کردیم.

اگر تستی که نوشتین پاس بشه باید در محیطی که دستور اجرای تست هاتون رو نوشتین با این پیغام مواجه بشین :

که در اینجا تعداد تست هایی که اجرا شدند و همچنین تعداد assertion(انتظاراتی که داشتین)های نوشته شده رو براتون مشخص میکنه.

اگه خسته نیستین اجازه بدین قبل از تموم کردن بحثمون یه توضیح مختصری در خصوص assertionها هم بهتون بدم.

ما با استفاده از assertion ها صحت تست هامون رو بررسی میکنیم. در واقع اگر بخوایم به زبان خودمون این توابع رو معنی کنیم به این صورت میگیم : من انتظار دارم status code تولید شده برابر باشه با مقداری که من بهت میدم( ()assertStatus )

یا مثلا من انتظار دارم در دیتابیس، داده ای که برات مشخص میکنم وجود داشته باشه ( ()assertDatabaseHas )

خب دیدید نوشتن یه تست چقدر راحته؟ به همین راحتی برای بقیه بخش های پروژتون هم میتونید تست بنویسید.

کلام آخر : تمرکز و هدف اصلیتون، اطمینان پیدا کردن از درست بودن ساز و کار برنامه ای که نوشتین باشه.

در تست نوشتن برای پروژه هایی که تموم شدن یا پیشرفت زیادی داشتن باید سعی کنید از ساده ترین بخش های پروژتون شروع کنید و کم کم برید سراغ قسمت های پیچیده پروژتون.

شاید اگر بار اولتون باشه که قرار هست برای یک سیستم تست بنویسید، ناخواسته وسواس زیادی به خرج بدین و مثلا قصد داشته باشین پیغام برگشتی از یک validation error رو هم بررسی کنید اما من بهتون میگم این کار نیاز نیست.

منتظر نظراتتون هستم..

موفق باشید.