آتش آزمون طلاست و سختیها، آزمون مردان قوی.
Seneca (c. 3 B.C.A.D. 65)
این جمله به این معنی است که نوشتن یک تست واحد بیشتر از طراحی است، تا تأیید. همچنین بیشتر از سند است، تا تأیید. نوشتن یک تست واحد باعث بسته شدن تعداد قابل توجهی از حلقههای بازخورد میشود، کمترین آنها مربوط به تأیید عملکرد است.
به عبارت ديگر، نوشتن يك تست واحد فقط براى اطمينان حاصل كردن از درستى عملكرد يك قسمت كد نيست، بلكه در واقع يك فرآيند طراحى و سندي است كه باعث بهبود كيفيت كدها مىشود.
۱. هیچ کد تولیدی نباید نوشته شود مگر اینکه یک تست واحد ناموفق نوشته شده باشد.
۲. بیش از حد کافی برای شکست یا عدم کامپایل شدن، تست واحد نباید نوشته شود.
۳. بیش از حد کافی برای پاس شدن تست واحد ناموفق، کد تولیدی نباید نوشته شود.
اگر به این شیوه کار کنیم، در چرخههای بسیار کوتاهی کار خواهیم کرد. فقط به اندازهای تست واحد مینویسیم تا شکست بخورد و سپس فقط به اندازهای کد تولیدی مینویسیم تا آن را پاس کند. هر چند دقیقه یک بار بین این دو مرحله جابجا خواهیم شد.
اولین و مشهودترین تأثیر این است که هر تابع برنامه دارای تستهایی است که عملکرد آن را تأیید میکند. این مجموعه از تستها به عنوان یک پشتیبان برای توسعه بعدی عمل میکند. هرگاه به طور ناخواسته قابلیت موجود را خراب کنیم، به ما این خرابی را اطلاع میدهند. ما میتوانیم قابلیتهای جدید به برنامه اضافه کنیم یا ساختار برنامه را تغییر دهیم، بدون اینکه نگران باشیم در فرآیند، چیز مهمی را خراب کنیم. تستها به ما اطلاع میدهند که برنامه همچنان درست عمل ميكند یا خیر. در نتيجة، آزادي بيشتري در ايجاد تغييرات و بهبود در برنامه ی خود داريم.
یک تاثیر مهم اما کمتر مشهود این است که نوشتن آزمون در ابتدا ما را به یک نقطه نظر متفاوت مجبور میکند. باید برنامهای را که قصد داریم بنویسیم از دیدگاه یک فراخواننده آن برنامه ببینیم. در نتیجه، به طور فوری با رابط برنامه همچنین عملکرد آن درگیر هستیم. با نوشتن آزمون در ابتدا، ما نرمافزار را به گونهای طراحی میکنیم که قابل فراخوانی باشد.
به عنوان مثال فرض کنید میخواهید یک برنامه بنویسید که دو عدد را جمع کند. با استفاده از روش توسعه مبتنی بر آزمون (TDD)، ابتدا یک آزمون برای این عملکرد مینویسید:
حالا که تست نوشته شده است، شروع به نوشتن تابع Add
میکنید:
پس از نوشتن تابع، تست را اجرا میکنید و اگر با موفقیت پاس شد، نشانهای از درست عملکردن تابع Add
خواهد بود.
علاوه بر این، با نوشتن آزمون در ابتدا، خود را مجبور میکنیم که برنامه را به گونهای طراحی کنیم که قابل تست داشته باشد. طراحی برنامه به گونهای که قابل فراخوانی و قابل تست باشد، بسیار مهم است. برای اینکه نرمافزار قابل فراخوانی و قابل تست باشد، باید از محیط اطراف خود جدا شود. در نتیجه، عمل نوشتن تست در ابتدا ما را مجبور میکند تا نرمافزار را جداسازی(decouple) کنیم!
یک تاثیر مهم دیگر نوشتن تست در ابتدا این است که تستها به عنوان یک شکل بینظیر از مستندسازی عمل میکنند. اگر میخواهید بدانید چگونه یک تابع را فراخوانی کنید یا یک شئ را ایجاد کنید، تستش وجود دارد که به شما نشان میدهد. تست ها به عنوان یک مجموعه از مثالهای کاربردی عمل میکنند که به برنامهنویسان دیگر کمک میکند تا بفهمند چگونه با کد کار کنند. این مستندات قابل ترجمه و قابل اجرا هستند. به روز خواهند بود. نمیتوانند دروغ بگوید.
فقط برای سرگرمی، من اخیراً یک نسخه از بازی Hunt the Wumpus نوشتم. این برنامه یک بازی ماجراجویی ساده است که در آن بازیکن در یک غار حرکت میکند و سعی میکند قبل از اینکه توسط Wumpus خورده شود، Wumpus را بکشد. غار شامل چندین اتاق است که با راهروهای متصل هستند. هر اتاق ممکن است به شمال، جنوب، شرق یا غرب راهرو داشته باشد. بازیکن با گفتن جهت حرکت به کامپیوتر، حرکت میکند.
یکی از اولین آزمونهایی که برای این برنامه نوشتم، تابع testMove (لیست 4-1) بود. این تابع یک WumpusGame جدید ایجاد میکرد، اتاق 4 را به اتاق 5 از طریق راهرو شرقی متصل میکرد، بازیکن را در اتاق 4 قرار میداد، دستور حرکت به شرق را صادر میکرد و سپس تأیید میکرد که بازیکن باید در اتاق 5 باشد.
این کد قبل از نوشتن هر بخشی از WumpusGame نوشته شده است. من به توصیه وارد کانینگهام عمل کردم و تست را به شکلی که میخواستم بخوانم، نوشتم. من اعتماد داشتم که میتوانم با نوشتن کدی که با ساختار پیشنهاد شده توسط تست مطابقت داشته باشد، تست را پاس کنم. این روش برنامهنویسی قصدی(intentional programming) نامیده میشود. شما قبل از پیادهسازی آن، قصد خود را در یک تست بیان میکنید و سعی میکنید قصد خود را به سادگى و نهایت خوانایى ممکن برسانید. شما اعتقاد دارید که این سادگى و وضوح به ساختار خوبى برای برنامه اشاره مىکند.
برنامهنویسی با هدف(intentional programming) من را به یک تصمیم طراحی جالب هدایت کرد. تست از کلاس Room، استفادهای نمیکند. عمل اتصال یک اتاق به دیگری هدف من است. به نظر نمیرسد که من برای تسهيل این ارتباط به یک کلاس Room نیاز داشته باشم. در عوض، من میتوانم به سادگى از اعداد صحيح برای نشان دادن اتاقها استفاده كنم.
وقتی با هدف برنامهنویسی میکنم، به یک تصمیم طراحی جالب رسیدم. در تست خود از کلاس Room استفاده نمیکنم. به جای آن، عمل اتصال یک اتاق به دیگری نشان میدهد که چه چیزی را مد نظر دارم. به نظر نمیرسد که وجود یک کلاس Room برای تسهيل این ارتباط لازم باشد. در عوض، من مىتوانم به سادگى از اعداد صحيح برای نشان دادن هر اتاق استفاده كنم.
بطور خلاصه، نويسنده در حال بيان است كه در تست خود، به جاى استفادۀ از يك كلاس Room، فقط با استفادۀ از عملكردهاى سادۀ ديگر مىتواند هدف خود را بيان كند.
این جمله به این معنی است که شاید برای شما عجیب به نظر برسد که چرا در طراحی این برنامه، کلاس Room وجود ندارد. زیرا ممکن است به نظر شما، این برنامه دربارۀ اتاقها، حرکت بین اتاقها، پیدا کردن چیزهایی که در اتاقها قرار دارد و غیره باشد. پس آیا طرحی که با هدف من نشان داده شده است، به خاطر عدم وجود یک کلاس Room نقص دارد؟
مفهوم اتصالات(Connect) در بازی Wumpus از مفهوم اتاق(Room) مهمتر است. این تست اولیه راه خوبی برای حل مسئله را نشان داد. در واقع، این نکتۀ اصلی نیست. نکتۀ اصلی آن است که تست در یک مرحلۀ بسيار سریع، يك مسئلۀ طراحى را روشن كرد. عمل نوشتن تستها در ابتدا روشی است كه باعث تشخيص تصميمات طراحى مىشود.
نوشتن تست ها در ابتدای طراحی یک برنامه می تواند به شما کمک کند تا مسائل مهم طراحی را در مرحله اولیه شناسایی کنید و بین تصمیمات طراحی تشخیص دهید. در اینجا، نویسنده با استفاده از یک مثال در بازی Wumpus، نشان میدهد که چگونه نوشتن تست ها در ابتدای طراحی باعث شده است که یک مسئله مهم طراحی را در مرحله اول شناسایی کند.
میتوانم بگویم که مفهوم اتصالات در بازی Wumpus از مفهوم اتاق مهمتر است. میتوانم بگویم که این تست اولیه راه خوبی برای حل مشکل را نشان داد. در واقع، فکر میکنم که این ظاهر کار است، اما نکتهای که سعی دارم بگویم این نیست. نکته این است که تست یک مسئله طراحی مرکزی را در یک مرحله بسیار سریع نشان داد.
توجه داشته باشید که تست به شما نحوه کار برنامه را میگوید. بیشتر ما میتوانیم به راحتی چهار متد نامبرده WumpusGame را از این مشخصات ساده استخراج کنیم و بنویسیم. همچنین میتوانیم سه دستور جهت دیگر را بدون زحمت زیاد نامگذاری ، شروع به نوشتن کنیم. اگر بعداً خواستید بدانید چگونه دو اتاق را به هم وصل کنید یا در جهت خاصی حرکت کنید، این تست به شما نحوه انجام آن را به صورت قطعی نشان خواهد داد. این تست عمل مستند قابل کامپایل و قابل اجرای توصيف برنامه است.
عمل نوشتن تست قبل از کد تولیدی اغلب مناطقی در نرمافزار را که باید جدا شوند(decoupled)، نشان میدهد. به عنوان مثال، شکل 4-1 یک نمودار UML ساده از یک برنامه حقوق و دستمزد را نشان میدهد. کلاس Payroll از کلاس EmployeeDatabase استفاده میکند تا یک شئ Employee را واکشی کند، از Employee میخواهد حقوق خود را محاسبه کند، آن حقوق را به شئ CheckWriter منتقل میکند تا چک تولید کند و در نهایت پرداخت را به شئ Employee پست میکند و شئ را دوباره در پایگاه داده ثبت میکند.
فرض کنید که ما هنوز هیچکدام از این کدها را ننوشتهایم. تاکنون، این نمودار به سادگی بعد از یک جلسه طراحی سریع روی تخته سفید قرار دارد.حالا ما باید تست را بنویسیم که رفتار شئ Payroll را مشخص کند. چندین مشکل در نوشتن این تست وجود دارد. اول، چه پایگاه دادهای استفاده میکنیم؟ Payroll نیاز دارد تا از نوعی پایگاه داده خوانده شود. آیا باید قبل از آزمایش کلاس Payroll یک پایگاه داده کاملاً عملکردی داشته باشیم؟ چه دادهای را در آن بارگذاری میکنید؟ دوم، چگونه تأیید میکنید که چک مناسب چاپ شده است؟ ما نمیتوانيم يك آزمون خودكار بنويسيم كه به صورت خودكار به پرينتر نگاه كرده و مقدار آن را تأييد كند!
راه حل این مشکلات استفاده از الگوی MOCK OBJECT است. ما میتوانیم رابطهایی(interfaces) بین همه همکاران Payroll قرار دهیم و تستهای stub ایجاد کنیم که این رابطها را پیادهسازی کنند.
شکل 2-4 ساختار را نشان میدهد. کلاس Payroll اکنون از رابطها(interfaces) برای برقراری ارتباط با EmployeeDatabase، CheckWriter و Employee استفاده میکند. سه MOCK OBJECT ایجاد شده است که این رابطها را پیادهسازی میکنند. این MOCK OBJECT ها توسط شئ PayrollTest پرسیده میشوند تا ببیند آیا شئ Payroll آنها را به درستی مدیریت کرده است.
لیست2-4 هدف تست را نشان میدهد. این تست MOCK OBJECT های مناسب را ایجاد میکند، آنها را به شئ Payroll منتقل میکند، به شئ Payroll میگوید تمام کارمندان را پرداخت کند و سپس از MOCK OBJECT میخواهد تأیید کند که تمام چکها به درستی نوشته شده و تمام پرداختها به درستی پست شده است.
درست است، این آزمایش فقط بررسی میکند که Payroll تمام توابع مناسب را با تمام دادههای مناسب فراخوانی کرده است. این آزمایش بررسی نمیکند که چکها نوشته شده یا پایگاه داده واقعی به درستی بهروز شده است. بلکه، این آزمایش بررسی میکند که کلاس Payroll در حالت جداگانه به درستی عمل میکند.
شما ممکن است درباره MockEmployee تعجب کنید. به نظر میرسد که کلاس Employee واقعی به جای یک mock قابل استفاده باشد. اگر اینطور بود، من هیچ تردیدی در استفاده از آن نداشتم. در این مورد، من فرض کردم که کلاس Employee پیچیدهتر از آنچه برای بررسی عملکرد Payroll لازم است، می باشد.
ادامه دارد......
پانوشت:
ترجمه ای آزاد از کتاب #Agile Principles,Patterns,and Practices in C