back-end developer
خود ره بگویدت که چون باید رفت
در دنیایی که تنها اصل ثابت تغییر است، برای اینکه نرم افزار شما همان چیزی باشد که از آن انتظار دارید تست نویسی تنها راه است. البته همیشه این نگرانی برای تیم های توسعه وجود دارد که تست نویسی موجب کند شدن روند تولید نرم افزار بشود و البته علاوه بر نیاز به مهارت های تست نویسی، پیچیدگی ها خود را نیز به سیستم اضافه میکند. اما کافیست با چگونگی تست نویسی آشنا شوید تا نه تنها برای نگرانی خود پاسخی بیابید بلکه دیگر حتی یک پروژه کوچک را هم بدون تست توسعه ندهید چرا که یافتن خطاهای سیستم نرم افزاری در مرحله توسعه و قبل از تبدیل به محصولی در دستان مشتری بسیار موثرتر و هزینه را کمتر خواهد کرد.
تست نویسی پس از طراحی سیستم اتفاق می افتد، جایی که نیاز مندیهایی سیستم مشخص شده اند و تیم توسعه میخواهد به سراغ پیاده سازی برود. در این مرحله با استفاده از نیازمندیهای سیستم تست ها را طراحی میکنیم.
تست نویسی ( چه UnitTest و چه تستهای مشتری ) بعد از پایان محصول و برای کدهایی که قابلیت تست را نداشته باشد کار سختی است، زمانیکه ابتدا سیستم را پیش از توسعه با تست نویسی شروع میکنیم (یعنی تست نویسی برای سیستمی که هنوز وجود ندارد)، تا انتها تمام کدهای شما (Production Code) بصورت سلسله مراتبی با قابل تست نوشته خواهند شد.
قابل تست بودن هر قسمت کوچک از سیستم باعث میشود کدهای شما بسیار مینیمال شوند و در نتیجه تکه های کدی که به هم وابستگی دارند بیشتر قابل شناسایی هستند چرا که با اجرای تست یک قسمت، قسمتی دیگر از تستها fail میشود که این وابستگی را نمایان میکند و میتوان قبل از پیچیده شدن وابستگی درون سیستم چاره ای برای آن اندیشید. بعلاوه احتمالا در طراحی سیستم نقص هایی وجود دارد که در توسعه سیستم همراه با تست این نقایص بروز میکنند چرا که روابط بین اشیا بهتر و بصورت عملی قبل از شروع کدنویسی دیده میشوند.
ابتدا باید بدانید چه چیزی را میخواهید تست کنید؟ (سناریوی تست) بعنوان مثال «تست عملکرد لاگین به سایت». سپس چگونگی تست را مشخص میکند. که معمولا نسبت به یک سناریو چندین مورد تست (Test Case) یا چگونگی داریم. بعنوان مثال «کاربر نام کاربری و رمز عبور خود را صحیح وارد میکند»، «کاربر رمز عبور خود را صحیح وارد نمیکند» و...
برای هر مورد تست (Test Case) باید مراحلی سپری شود که از انجام آنها باهم عملکرد یک قسمت از سناریو کامل میشود. بطور مثال : ۱. وارد کردن نام کاربری ۲. وارد کردن رمز عبور ۳. زدن دکمه ورود
عموما تست ها برای انجام نیاز به داده های ورودی دارند که این ورودی ها در سیستم بر اساس شرایطی بوجود می آیند که پیش شرط های تست هستند. بطور مثال میتوان گفت «کاربر برای ورود به سیستم باید از قبل در سیستم ثبت شده باشد» پس باید برای تست ورود کاربر شرایط را در حالتی که کاربر ثبت نام کرده در نظر میگیریم. یعنی در نظر میگیرم اطلاعاتی که کاربر از آن بعنوان نام کاربری و رمز عبور وارد خواهد کرد باید در دیتابیس وجود داشته باشد.
هر مورد تست باید با داده های مختلف تست شود تا احتمالا رفتار غیر قابل پیش بینی سیستم نیز کشف شود. به طور مثال به نام کاربری «chandler» نیاز داریم و رمز عبور «chandlerpassword123»
در نهایت باید حتما از عملکرد تست چیزی را بعنوان نتیجه انتظار داشته باشیم. بطور مثال «ورود به صفحه اصلی» و یا «نمایش پیام خوش آمدگویی»
بوضوح میتوانیم ببینیم که برای تست هر عملکرد سیستم به بازبینی نیازمندیهای سیستم و در نظر گرفتن روابط بین اجزای درون سیستم با همدیگر است.
ولی سودای نوشتن یک جای تمام تست های سیستم را فراموش کنید. در واقع بهتر است از توسعه افزایش تست استفاده کنیم « یکم تست نویسی یکم کد نویسی ». اینگونه تمرکز ما بر روی یک تست مردود شده (Failed) خواهد بود.
اما در مورد پیچیدگی های فنی، باید شرایطی که تیم تست نویسی را از تیم توسعه جدا تصور کنیم بطوریکه کد تست را از روی یک دیوار بلند به سمت توسعه دهندگان بیاندازند و منتظر پاس کردن آن ها شویم را باید فراموش کنید. در واقع توسعه دهندگان و تست نویسان با همکاری همدیگر سیستم را پیش میبرند.
اما تست نوشتن برای هر عملکرد از سیستم بهره وری را کاهش خواهد داد زیرا ما در واقع داریم یک پروژه سایه برای تست که به اندازه بزرگی پروژه اصلی است را تولید میکنیم و این توسعه را بسیار کند میکند. شما باید بدانید که این تست نیست که برای شما معجزه میکند در واقع این روش شما است که معجزه میکند.
ما میتوانیم با استفاده از ترکیبی از TDD و BDD همه سیستم را تست کنیم اما نباید با هر کدام از این روشها کل سیستم را تست کنیم. زیرا که هر کدام کارایی خود را دارند. بطور کلی BDD رفتار کاربر و چگونگی انجام سیستم را براساس سناریوی مشتری بررسی میکند اما TDD از اینکه چه چیزی را چه وقت تست کنید حرف میزند. تست های رفتاری BDD میتواند در انتهای کار ضمانت انجام فرایندهای کلیدی سیستم را برعهده گیرد. اما چیزی که حجم زیادی از تست های شما را دربرمیگیرد و ممکن است موجب دردسر شود unit test ها هستند.
اگر میخواهید همه چیز را تست نکنید و حجم تست های unit خود را کم کنید پس بصورت رویه ای کد نزنید تا مجبور نباشید پیوستگی های خارج از کنترل را به اجبار تست کنید. راه حل در دستان روش شی گرا است چرا که رفتار سیستم توسط کلاس ها از هم جدا میشوند. در طراحی شی گرا، سیستم بر اساس پیام هایی که بین کلاس ها فرستاده میشود کار میکند. هر شی سه نوع پیام را تبادل میکند : پیام های ورودی، پیام های درونی (جهت کار کردن اجزای درونی سیستم با همدیگر)، و پیام های خروجی(پیامد)
براساس گفته ی sandi metzِ، بطور کلی میتوانیم پیام های بین کلاس ها را بر اساس عملکرد به دو نوع استاندار طراحی کنیم:
Query: مقداری را برمیگردانند اما چیزی را نباید تغییر دهند.
Command: مقداری را تغییر میدهند اما نباید چیزی را تغییر دهند.
البته بعضی از اوقات رعایت این تقسیم بندی ممکن نیست. بطور مثال عملکرد صف را در نظر بگیرید. عمل Pop باعث برگرداندن آخرین مقدار است و همچنین عمل تغییر صف به حالت جدید و با یک مقدار کمتر است. این حالات که هم query وهم command است را استثنا در نظر میگیریم. حال ما میتوانیم بر اساس نوع پیام ها جدول زیر را داشته باشیم.
ابتدا سراغ پیام های ورودی از نوع query میرویم، در این نوع کلاس ها آنچیزی که باید برگردانده شود را با استفاده از assertion تست میکنیم. در اینجا نکته این است که باید آنچیزی که از بیرون کلاس میبینم را تست کنیم و از تست کردن نحوه پیاده سازی آن بپرهیزیم.
در کلاس wheel برای محاسبه قطر متد diameter را داریم:
class wheel
attr_reader :rim,:tire
def initialize(rim,tire)
end
def diameter
rim + (tire *2)
end
end
و تست:
class WheelTest < MiniTest::Unit::TestCase
def test_calc_diameter
wheel = wheel.new(26,1.5)
assert_in_delta(29,wheel.diameter,0.01)
end
end
حال سراغ پیام های ورودی از نوع command میرویم، در این نوع پیام ها که مقداری تغییر میکند باید با استفاده از assertion عوارض جانبی ای که بصورت عمومی کل سیستم از آن میتوانند مطلع باشند اما الزاما فقط همین کلاس باید نسبت به آن مسئول باشد را تست کرد.
Set_cog محاسبه ای را با استفاده از پیام ورودی انجام میدهد
class Gear
attr_reader :chairing,:cog,:wheel
def initialize(args)
end
def set_cog(new_cog)
@cog = new_cog
end
end
class GearTest < MiniTest::Unit::TestCase
def test_set_cog
gear = Gear.new
gear.set_cog(27)
assert(27,gear.cog)
end
end
حال به سراغ پیام های کلاس به خود میرویم. جایی که شاید بصورت عموما متدهای Private تعریف میکنیم. این متدها به خود کلاس و در متدی دیگر صدا زده میشوند و به آنها پاسخگو هستند و نه به دنیای بیرون از کلاس پس چه از نوع query و چه از نوع command تست کردن انها را کنار بگذارید.
البته اگر هم کسی این قسمت ها را تست کند اشکالی به کار او وارد نیست ولی او فقط حجم کار خود را زیاد کرده است.
برای پیام ها خروجی از نوع query در نظر داشته باشید که احتمالا این نوع پیام ها احتمالا ورودی پیام دیگری هستند که باید در همان ورودی تست شوند پس برای جلوگیری از تکرار این جا هم تستی نباید انجام شود.
اما در مورد پیام خروجی که از نوع command است باید فقط تا جایی که در سیستم عوارض جانبی دارد را تست کرد و بقیه کار را فقط بصورت Mock کردن قسمتی که اولین تاثیر را بر آن میگذارد انجام داد (همین که متدی در ادامه صدا زده میشود کافی است) و بقیه پیاده سازی را باید به خودش واگذار کرد. در اینجا اساس کار بر اعتماد اشیا به همدیگر است زیرا ما در جایی دیگر این پیامد را بعنوان ورودی کلاسی دیگر تست خواهیم کرد. و البته باقی ماجرا را به تست های integration نیز میتوان سپرید.
مطلبی دیگر از این انتشارات
استعفا از شغل برنامه نویسی بهترین تصمیم زندگیم! #نه_به_برنامه_نویسی
مطلبی دیگر از این انتشارات
یادگیری ماشین با پایتون - sklearn (قسمت اول)
مطلبی دیگر از این انتشارات
چند مورد از Best Practice های توسعه در جنگو به همراه جایزه