توسعه مبتنی بر تست (TDD)

مطلبی که می‌خوانید ترجمه‌ قسمت ۱۵۵ از رادیو مهندسی نرم‌افزار است. رادیو مهندسی نرم‌افزار هر یکی دو هفته یک بار مصاحبه‌ای درباره‌ی یکی از موضوعات حوزه‌ی مهندسی نرم‌افزار با افراد خبره و با تجربه در موضوع مورد بحث ترتیب می‌دهد

در این قسمت، یوهانس لینک با لاسی کاسکلا نویسنده کتاب TDD در ارتباط با توسعه مبتنی بر تست (Test Driven Development) مصاحبه می‌کند. در این مصاحبه، مبانی TDD، منطق پشت آن و چالش‌های انجامش در محیط‌های دشوارتر بحث می‌شود.

قبل از اینکه وارد مصاحبه شوم می‌خواهم که در زمینه زندگی حرفه‌ای خودت کمی برایمان صحبت کنی.

من یک خوره‌ کامپیوتر هستم. برای تقریباً یک دهه در زمینه‌ای که عمدتاً برنامه‌نویسی بوده، کار می‌کرده‌ام. در ۴-۵ سال گذشته اختصاصاً با روش‌های چابک در حوزه مربی‌گری هم برای سازمان‌ها و هم در سطح تیم‌ها، کار می‌کرده‌ام اما هیچ گاه برنامه‌نویسی را کنار نگذاشته‌ام به عنوان مثال تا کنون مقدار زیادی تجربه‌های زیبای برنامه‌نویسی دونفره (Pair) داشته‌ام.

هنوز هم بیشتر وقتتان را به برنامه‌نویسی می‌گذرانید؟

بله. در واقع، ابتدای سال تصمیم گرفتم که به یک تیم برای مدت بیشتری بپیوندم. الان مدت ۷ ماه است که عضو یک تیم هستم.

و آیا این خوب است؟ اینکه فقط کد بزنیم؟

بله. فکر می‌کنم زمانش رسیده بود. در ۵ سال قبل از این، من با هر تیمی به طور ماکزیمم دو هفته کار می‌کردم و بعد جای دیگری می‌رفتم. البته ممکن بود برگردم اما این فرق می‌کند.

علتی که من شما را دعوت کرده‌ام این است که شما کتابی منتشر کرده‌اید. فکر می‌کنم در سال ۲۰۰۸ بوده است.

در واقع سال ۲۰۰۷.

بله، یک کتاب در مورد توسعه‌ به روش مبتنی بر تست (Test Driven Development). عنوانش چه بود؟

همین، مبتنی بر تست

اینکه چیزی مبتنی بر چیز دیگر جلو رود تا حدی با اعتیاد به آن همراه می‌شود. آیا شما به نوعی معتاد تست هستید؟

تا اندازه‌ای بله. قطعاً اگر بدون تست کار کنم، احساس ناخوشایندی دارم. بنابراین، بله می‌توان من را معتاد تست خواند.

آیا شده که برخی مواقع بدون تست کار کنید؟

متأسفانه هر از چند گاهی، در چنین وضعیتی قرار می‌گیرم. به عنوان مثال، در کاری که هم‌اکنون انجام می‌دهم کدهای میراثی (Legacy Code) هستند که نقاط زیادی از آن پوشش کافی تست ندارد. همچنین برخی تکنولوژی‌های استفاده‌شده، نوشتن تست را سخت کرده است. من احساس غریبی دارم. اگر با تست کار می‌کردم احساس راحتی و لذت خیلی بیشتری داشتم.

روش TDD مدتی است که مطرح شده است اما هنوز افراد مختلف وقتی این لغت را استفاده می‌کنند، منظورهای مختلفی دارند. شما چطور TDD را تعریف می‌کنید؟

من اعتقاد دارم اولین بار، TDD در نوشته‌های انتشاریافته تعریف شد. TDD بیشتر به برنامه‌نویسی به روش تست اول (Test First Programming) اشاره داشت؛ ترکیبی از ایده‌های تست و ایده‌ پیاده‌سازی حداقل (ساده‌ترین چیزی که کار مورد نظر را انجام دهد) و سپس تست بعدی و پیاده‌سازی بعدی و به این شکل پیش بردن طراحی.

آیا در مورد چرخه TDD فکر نمی‌کنید؟

اولین تعریف TDD ترکیبی از این دو موضوع بود که: اول تست را می‌نویسید و بعد پیاده‌سازی می‌کنید و دیگر تأکید بر طراحی ناشی‌شده از آن (Emergent Design). بنابراین طراحی‌تان به شدت تحت تأثیر تست، نمو پیدا می‌کند. من برای TDD یک تعریف ساده‌تر در ذهنم دارم. قطعاً منظورم مفهوم «تست اول» هست ولی شما حتماً قبل از آنکه آغاز کنید در مورد طراحی فکر می‌کنید. شما حتماً ایده‌ای از این که کدتان را به کجا می‌خواهید ببرید دارید اما با این وجود اجازه می‌دهید که کد به شما بگوید که آن، پاسخ کار را نمی‌دهد و ممکن است به جهت‌های دیگر برویم. بنابراین تعاریف مختلفی که کم ‌و بیش از خودم هست دارم.

مؤلفه‌های TDD کدامند؟ شما تا حالا، به «ابتدا تست را نوشتن» و «اجازه دادن به کد که مسیر بعدی را نشان دهد» اشاره کردید. من می‌توانم چیزهای دیگری از قبیل بازسازی (Refactoring) بعد از تست یا در حین تست را متصور شوم.

بله. اساساً چرخه TDD، عبارتست از: «تست نوشتن، کد زدن و بازسازی (Refactor) کردن». این‌ها قطعاً مؤلفه‌ها هستند. این‌ها چیزهایی است که وقتی شما TDD کار می‌کنید همواره انجام می‌دهید.

آیا در مورد TDD فکر می‌کنید چیزی وجود دارد که آن را به قطع، الزامی می‌کند؟

بله. فکر می‌کنم وجود دارد. تحقیقات و مقالات مختلفی در مورد مؤثر بودن TDD، تأثیر TDD بر روی کیفیت کد و ... وجود دارد. اگر به آن دسته از تحقیقاتی که اظهار می‌کنند که ممکن است TDD تأثیرات منفی در کیفیت داشته باشد، نگاه کنید؛ وقتی خود نتایج واقعی را نگاه می‌کنید می‌بینید که برخی شاخص‌های شیء‌گرایی هستند که امتیاز پایین‌تری آورده‌اند. این واقعاً به علت استفاده از TDD به عنوان روش توسعه نیست بلکه به علت فقدان مهارت طراحی شیءگرا است. اساساً TDD موتوری است که یک ریتم طبیعی برای ایجاد یک طراحی شیءگرای خوب ایجاد می‌کند. مشکل این‌جا است که TDD جادویی نیست که این حس را به شما بدهد که طراحی شیء گرای خوب چیست. این یک مؤلفه فراموش‌شده یا ذکرنشده TDD است.

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

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

بیایید به این موضوع برگردیم که چگونه تست می‌تواند طراحی را بهبود دهد. من ابتدا به مباحث اولیه برمی‌گردم. شما گفتید اولین مرحله از چرخه TDD، نوشتن تست است. روشن است که این تست در مورد چیزی است که هنوز کار نمی‌کند. درست است؟

بله.

چگونه می‌توانیم چیزی را که هنوز کار نمی‌کند یا حتی وجود ندارد را تست کنیم؟

این به آن علت است که زمانی که ما در TDD از تست صحبت می‌کنیم، معنای دیگری به این کلمه می‌دهیم. وقتی ما به عنوان بخشی از چرخه «تست، کد و بازسازی»، تست می‌نویسیم، در واقع، چیزی را ارزیابی نمی‌کنیم بلکه یک خواسته را بیان می‌کنیم، چیزی که مفقود است را بیان می‌کنیم. به نوعی بیشتر در حوزه ذکر مشخصات (Specification) هستیم تا اینکه در حوزه ارزیابی (Verification) باشیم. به سادگی، تنها به چیزی که در پیاده‌سازی فعلی اشتباه است یا مفقود است اشاره می‌کنیم.

مفقود از نظر نیازمندی‌ کاربران؟

بسته به کدتان دارد. گاهی مواقع می‌تواند مربوط به چیزی کاملاً سطح پایین و تکنیکی باشد. ما با بخش‌هایی از کد که اشیاء هستند سروکار داریم. هر از چند گاهی بخشی از کدی که با آن کار می‌کنید به حوزه کاربران خیلی نزدیک می‌شود. بنابراین بستگی دارد.

بنابراین شما می‌گویید که اکثر مواقع تست‌هایی که می‌نویسید، می‌توانند به نیازمندی‌‌های کاربران ترجمه شوند یا اگر از جهت دیگر بگوییم، نیازمندی‌های کاربر به چنین تست‌هایی تبدیل شوند؟ یا اینکه موارد اندکی هست که از دید خارجی سیستم کار کنید؟

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

وقتی می‌گویید «نعمت»، به این معناست که این روش بهتری است؟

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

شما به منحنی یادگیری (Learning Curve) اشاره کردید. روشن است که در ابتدای کار باید زمانی بوده باشد که شما جذب TDD شده‌اید و گفته باشید: بله، حس می‌کنم که به من سود می‌رساند. آیا آن زمان را به خاطر می‌آورید؟

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

آیا تجربیات کاری شما بعد از آن زمان که به امید رسیدید، تغییرات اساسی کرد؟

قطعاً این‌طور بود. استرس زیاد در من متوقف شد. به عنوان مثال در پروژه‌ای که الان کار می‌کنم هر دو هفته یک‌بار، نسخه منتشر می‌‌کنیم. من دیگر استرس زیادی برای این ندارم که تعداد زیادی خطا در آن نباشد. خطاهایی که به دستمان می‌رسد خیلی کمتر و کم‌اهمیت‌تر شده است. در واقع در مقایسه با مثلاً ۸ سال پیش خیلی استرس کمتری دارم.

وقتی می‌گویید تعداد خیلی کمی خطا دارید، آیا می‌توانید در مورد تعدادشان به ما بگویید؟

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

فکر می‌کنم ندیدم شما از اصطلاح تست واحد (Unit Test) استفاده کنید. گفتید تست و گفتید بیان مشخصات (Specification). اما اغلب افراد از اصطلاحات تست واحد و TDD به نوعی به شکل مترادف استفاده می‌کنند. شما چطور فکر می‌کنید؟ آیا یکسان هستند یا اینکه انجام تست واحد، کاملاً متفاوت با TDD است؟

من اصطلاح تست واحد (Unit Test) را خیلی دوست ندارم! هر از چند گاهی از این اصطلاح استفاده می‌کنم ولی این مشکل وجود دارد که وقتی به یک سازمان می‌روید، این اصطلاح می‌تواند معانی کاملاً متفاوتی در مقایسه با دیگر سازمان‌ها یا معنی موردنظر شما داشته باشد. اول از همه: واحد چیست؟ ممکن است بگویند ما از فلان تکنولوژی استفاده می‌کنیم و شیء یا کلاس نداریم. در اینصورت واحد چیست؟ به عنوان مثال ممکن است در آنجا واحد، فایل‌های منبع (Source) باشد. به غیر از آن، در سازمان‌های مختلف تست واحد معمولاً مربوط به سطوح خیلی ریز است که در یک زمان، تنها با یکی دو کلاس سروکار دارد. بنابراین اولین مورد مشکل‌زا بودن اصطلاحات است. مورد دوم این است که من فکر می‌کنم نمی‌توان حکم کرد که وقتی با روش مبتنی بر تست (Test Driven) کار می‌کنید باید با نوع خاصی از تست کار کنید اگرچه عمده تست‌هایی که هنگام TDD کار کردن می‌نویسم طوری هستند که می‌توان آنها را تست واحد نامید.

یعنی اینکه خیلی ریزدانه هستند و در ارتباط با یک متد یا شیء مجزا هستند؟

بله. اغلب مواقع، تست‌هایی که می‌نویسم شامل تعداد اندکی شیء می‌شود. اگر تأثیرات جانبی (Side Effect) داشته باشند، اطمینان می‌یابم که آن را پاکسازی (Clean-up) کرده باشم تا اینکه برای انجام تست بعدی، تأثیرات پاک شده باشد. اغلب موارد با تعداد اندکی شیء، تست می‌نویسم که یک کار خیلی کوچک خاص و مجزا را انجام می‌دهد اما هر از چند گاهی انواع دیگری از تست هم می‌نویسم. به عنوان مثال ممکن است بخواهم دسته‌ای از مؤلفه‌های گرافیکی را برپا کنم: یک برنامه خیلی کوچک GUI که چند مؤلفه گرافیکی دارد که در کنار هم وجود دارند و با هم تعامل دارند. طبق تعریف دقیقش، اینها تست واحد نیستند ولی همچنان فکر می‌کنم که معنی می‌دهد که این مورد خاص را هم به صورت مبتنی بر تست انجام داد. در چنین شرایطی، چنین روشی می‌تواند بهترین باشد.

یعنی واقعاً GUI را تست می‌کردید؟

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

من فکر می‌کنم برای انجام تست در آن سطح به غیر از JUnit یا nUnit ساده به ابزارهای دیگری هم نیاز دارید تا بتوانید فرضاً دکمه را کلیک کنید و کارهای این چنینی انجام دهید.

بله. اگر شما زیاد این کار را می‌کنید فکر می‌کنم باید حداقل استفاده از نوعی فریم‌ورک را درنظر بگیرید و مراقب باشید این کار را به یک کابوس (در مرحله‌ی نگهداری از نرم‌افزار) تبدیل نکنید.

اما شما به ابزارهای مختلفی مانند تکنولوژی‌های ضبط - بازپخش (Capture - Replay) نیاز ندارید؟

ضبط - بازپخش، ایده خوبی است. اما مشکلی که اینجا وجود دارد این است که چیزی برای ضبط ندارید. اگر دکمه‌ای در جایی مفقود است چه طور می‌خواهید ضبط کنید که اگر دکمه کلیک شد اتفاقی بیافتد. اصلاً دکمه‌ای نداریم.

بله. نکته ظریفی است. من می‌شنوم که افراد در جاهای مختلف از TDD استفاده می‌کنند: Java، .Net، Phyton، Rubby. آیا تکنولوژی می‌شناسید که برای TDD مناسب نباشد؟

قطعاً با برخی تکنولوژی‌ها، نوشتن تست خیلی سخت‌تر می‌شود. به عنوان مثال پارسال من در بنگلر هند بودم. من داخل یک تیم جاوا بودم اما اتاق کناری ما یک گروه بودند که بر روی کد C کار می‌کردند. ما علاقه داشتیم که برویم و کمک‌شان کنیم. جالب بود. من برنامه‌نویس C نیستم. من در مورد همه کارهایی که آن‌جا می‌شود کرد اطلاع نداشتم. ما مقداری پیشرفت کردیم اما مسأله این است که زبان برنامه‌نویسی کار نوشتن مجموعه تست‌ها را خیلی سخت می‌کند. قطعاً می‌توانید این کار را انجام دهید اما API ها و فریم‌ورک‌هایی که آن‌جا فراهم است نسبت به JUnit و nUnit و این گونه ابزارها خیلی سطح پایین‌تر هستند.

این یک مطلب است. مطلب دیگر این است که وقتی من با زبان جاوا در IDE های مدرن مانند Eclipse کار می‌کنم، من تایپ می‌کنم و تایپ می‌کنم و بعد بلافاصله تست‌ها را اجرا می‌کنم. من کامپایل نمی‌کنم چون IDE آن را به شکل پیوسته انجام می‌دهد و کار آن قدر آنی انجام می‌شود که نیازی نیست به آن فکر کنید اما وقتی که بعنوان مثال با C کار می‌کنید، کامپایل کردن خودش چیز بزرگی است. وقتی می‌خواهید تست‌ها را اجرا کنید باید ابتدا کامپایل کنید و بعد صبر کنید. ممکن است ۲-۳ ثانیه یا ۱۰ ثانیه طول بکشد و بعد از آن تست‌ها را اجرا می‌کنید. بنابراین چرخه فیدبک به جای ۱ ثانیه ممکن است ۱۰ ثانیه به طول بیانجامد. این مطلب اساساً روش اجرا کردن چرخه TDD را عوض می‌کند. یکی از دوستانم در مورد این روش جدید می‌گوید که TDD به روش خوش‌بینانه است به این ترتیب که پس از آن‌که کامپایل و اجرای تست را آغاز کردید، بلافاصله به تایپ کردن‌تان ادامه می‌دهید. در پس‌زمینه کامپایل در حال انجام است و وقتی کامپایل تمام شد، تست‌ها اجرا خواهند شد اما شما به کارتان ادامه می‌دهید. شما با پیش‌فرضی که در مورد قبول شدن یا ردشدن تست‌ها دارید، کارتان را ادامه می‌دهید. شما فرض می‌کنید که پیش‌فرض و درکتان از تست‌ها صحیح است. اغلب مواقع، این روش کار می‌کند اما هر از چند گاهی می‌بینید که تست رد نشده است اما قرار بود که رد شود یا تست قبول نشده است اما قرار بوده قبول شود؛ بنابراین پیش‌فرض اشتباهی داشته‌اید و مجبورید که ذهن و کدتان را به عقب، به بعد از تست قبلی ببرید. بنابراین می‌توانید آن را انجام دهید ولی جور دیگری است و حس دیگری دارد.

آیا فکر می‌کنید که در این گونه محیط‌ها TDD همچنان ارزش دارد؟ یا فکر می‌کنید که چه مدت اگر چرخه فیدبک طول بکشد ارزش دارد که همچنان از TDD استفاده کنیم؟

۴۲ خوبه! (به شوخی). ایده‌ای ندارم. من محیطی ندیده‌ام که TDD را سودمند نیافته باشند. یا بگوییم تا حدی سودمند چون به هر شکل، این مربوط به اشخاص است.

آیا در محیط‌های ++C از نوعی که ۱۵ دقیقه یا حتی بیشتر طول بکشد تا سیستم Build شود بوده‌اید؟

شخصاً خیر. من با برخی افراد صحبت کرده‌ام که در شرایط خیلی جالب کار می‌کرده‌اند مثلاً در بانک لندن که با ++C کار می‌کرده‌اند. آن‌جا مجموعه ابزارهایی مانند BI یا Emacs هست. آنها در IDE های پیچیده خود، امکانات بازسازی (Refactoring) را ندارند. در واقع IDE وجود ندارد. با این وجود TDD کار می‌کردند. برای مثال بازسازی را با نوشتن Shell Script هایی انجام می‌دادند که فایل‌های کد برنامه‌ را بوسیله عبارات منظم (Regular Expression) دستکاری می‌کرد: ابزارهایی از قبیل sed و awk . این کار به نظر خیلی پرزحمت می‌آید اما آن‌ها تصمیم گرفتند که این کار را بکنند و آن را ادامه دادند. بنابراین واضح است که این کار برایشان سود داشته است حتی با وجود اینکه بر روی یک سیستم قدیمی کار می‌کرده‌اند و از تکنولوژی‌های دشواری استفاده می‌کرده‌اند و ++C دوستانه‌ترین زبان برای برنامه‌نویسان نیست. شخصاً ندیده‌ام اما شنیده‌ام که خیلی افراد می‌گویند که گرچه خیلی ترسناک به نظر می‌رسد اما ما واقعاً آن را انجام می‌دهیم و راضی هستیم.

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

سخت است که بگوییم.

به جنبه‌های فنی‌تر برگردیم. به عنوان مثال شما گفتید که گاهی مواقع از تست واحد (Unit Test) محض و تست مجزای چیزها، پیروی نمی‌کنید. وقتی می‌خواهید این کار را بکنید و چیزهایی از قبیل سیستم‌های ساختگی یا پایگاه داده دارید، احتمالاً می‌خواهید آن شیء که در حال تستش هستید را از چیزهای دیگر مثلاً پایگاه داده و زیرساخت‌ها، مجزا کنید. برای آن چه کار می‌کنید؟

یکی از تکنیک‌های اصلی برای این کار، تزریق وابستگی‌ (Dependency Injection) است. برای این کار در زبان جاوا، از استفاده از کلمه new اجتناب می‌کنید. می‌خواهید پیوستگی کد تحت تست از پیاده‌سازی مؤلفه‌های همکارش را از بین ببرید. برای مثال به جای اینکه بگویید ()this=new widget آن widget و وابستگی را به تابع سازنده، می‌فرستید. به این ترتیب در تست‌تان می‌توانید، یک شیء را با تعدادی نمونه‌های قلابی از وابستگی‌هایش بسازید. به عنوان مثال با بهره‌گیری از اشیاء مقلد (Mock Object). فکر می‌کنم این، مهمترین تکنیک ضروری در این‌جا است.

آیا تکنیک سختی است؟ من استفاده از لغت شیء مقلد (Mock Object) را برای موارد بسیار گوناگونی شنیده‌ام. برخی مواقع من را گیج می‌کند.

من آن را اینقدر دشوار نمی‌بینم. مشتاقم در مورد گیج شدن در ارتباط شیء مقلد بدانم.

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

درست است. چندین اصطلاح در این مورد وجود دارد. یکی از آن‌ها ابتدا توسط مارتین فاولر نگاشته شده است. چند سال پیش او مقاله‌ای با عنوان Mocks or Stubs نوشت. فکر می‌کنم اسمش این بود. می‌توانید چک کنید. (عنوان مقاله Mocks Aren't Stubs است و از این آدرس در دسترس است - مترجم). او این دسته‌بندی را انجام داد که فکر می‌کنم کاملاً جهانی باشد. شیء خُرد (Stub) وجود دارد و شیء مقلد (Mock) هم وجود دارد. شیء خُرد (Stub)، ساده‌ترین پیاده‌سازی ممکن از یک واسط است. متدهایش کاری نمی‌کنند صرفاً یک مقدار ثابت null یا صفر یا ۱۵ یا ... برمی‌گردانند. اصولاً همه چیزش هاردکد است. همچنین اشیاء قلابی (Fake) هم وجود دارند که کم‌ و بیش یک پیاده‌سازی سبک‌تر از شی‌ء واقعی هستند. اصولاً وقتی یک شیء قلابی می‌گیرید و آن را فراخوانی می‌کنید مانند شیء واقعی عمل می‌کند اما واقعی نیست.

به عنوان مثال اگر یک پایگاه داده قلابی داشته باشم، واقعاً مقادیر و ستون‌ها را داخل خودش نگهداری می‌کند؟

یک پایگاه داده قلابی (Fake) می‌تواند یک نگاشت درهم‌سازی (Hash Map) یا چیزی باشد که مبتنی بر آن ساخته شده است. چیزی را واقعاً بر روی دیسک ذخیره نمی‌کنید درعوض آن‌ها را بر روی حافظه نگه می‌دارید و وقتی جستجو می‌کنید، ممکن است یک دور روی همه اشیاء بزنید تا اینکه به چیزی که دنبالش هستید، برسید.

یعنی به نوعی شیء اصلی را شبیه‌سازی می‌کند؟

بله، می‌توانیم به این شکل بگوییم. و بعد دسته سوم را داریم که اشیاء مقلد (Mock) هستند. که کمابیش قابل تنظیم هستند. شما برای یک واسط (interface) یک شیء مقلد می‌گیرید و می‌توانید به آن بگویید که وقتی فلان اتفاق افتاد، فلان رفتار را بکن. بعنوان مثال می‌توانید بگویید اگر یک اتفاقی افتاد باعث رد شدن تست شود. من فکر می‌کنم این دسته‌بندی در مورد بدل‌های تست (Test Double) کاملاً سراسری است و پذیرفته شده جهانی است.

آیا فریم ورک تست شما، امکانات لازم در ارتباط با اشیاء مقلد را می‌دهد یا از ابزارهای دیگر برای این کار استفاده می‌کنید؟

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

بنابراین عموماً نیاز به یک فریم‌ورک دیگر خواهم داشت؟

اصولاً بله. نمی‌توانید خودتان شیء مقلد را بنویسید. کار ملالت‌باری می‌شود. بله، نیاز به یک فریم‌ورک یا کتابخانه دارید.

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

یک مثال از جایی که این کار را می‌کنید، شیء دسترسی به داده (Data Access Object) است. یک بار من بر روی برنامه‌ای کار می‌کردم که بر روی یک پایگاه داده رابطه‌ای می‌نشست و شیء دسترسی به داده را داشتیم که مسئول جابجایی داده‌ها از پایگاه داده و قرار دادن آنها در اشیائی است که بتوانید با آنها کار کرده و دستکاری‌شان کنید. یا برعکس ممکن است بخواهید که یک شیء آن حوزه (Domain) را بدهید و بخواهید که آن را در پایگاه داده ثبت (Persist) کنید. حالا، چگونه این‌ها را به روش مبتنی بر تست (Test Driven) تست می‌کنید؟ چند روش وجود دارد به عنوان مثال در جاوا، می‌توانید اینترفیس‌های JDBC را تقیلد (Mock) کنید یا قلابی‌اش (Fake) کنید یا این‌ که می‌توانید یک پایگاه داده واقعی، برپا کنید و تست‌ها را روی آن اجرا کنید. این مثالی است که به نظر من به طور پیش‌فرض، احتمالاً از پایگاه داده استفاده می‌کنید.

ولی فکر نمی‌کنید که زمان زیادی طول می‌کشد مثلاً راه‌انداختن Oracle نیم‌دقیقه طول می‌کشد و باز کردن چند جلسه (Session)، هر کدامشان، ۱۰ ثانیه دیگر طول بکشد...

بله، در واقع در این صورت من از پایگاه داده‌های سبک‌ مانند H2 یا HSQL استفاده می‌کنم. تعدادی پایگاه داده‌ داخل حافظه (In-Memory) وجود دارد که واقعاً سبک هستند و درکسری از ثانیه برپا می‌شوند. ممکن است تأثیراتی داشته باشد اما واقعاً همانند Oracle و MySQL پایگاه داده‌های مناسبی هستند.

اما به عنوان مثال اگر با Hibernate کار کنید، زبان پرس‌وجو خاص خودش را دارد. من قطعاً یک خبره Hibernate نیستم. من در به‌ خاطر آوردن این که چطور یک کار را با زبان پرس‌‌وجوی Hibernate می‌شود انجام داد، به مشکل می‌خورم. بنابراین نمی‌شود که همه چیز را قلابی کنم. در نظر بگیرید تستی بنویسم که: آقای «شیء دسترسی به داده»، اگر این کار را با تو کردم، آن‌گاه تو باید این فراخوانی API را بر روی واسط Hibernate داشته باشی و من به این روش، آن را قلابی (Mock) کرده‌ام. اگر به این روش کار کنم آن وقت، نمی‌دانم که آیا نحو (Syntax) آن جمله خاص، درست بوده است یا خیر. بنابراین من ترجیح می‌دهم که بر روی پایگاه داده واقعی کار کنم که بتوانم تست‌های معنی‌داری بنویسم که بتواند چیزی را برایم مشخص کند.

چیزی که از این برداشت می‌کنم این است که هرگاه بخواهید از شیء مقلد (Mock) برای تقلید چیزی استفاده کنید باید واقعاً آن چیز را به خوبی بشناسید. حداقل در مورد API ها باید بدانید که چطور باید از آنها استفاده شود. در غیر این صورت، نمی‌توانید شیء مقلد صحیحی برای آن بنویسید.

دقیقاً. این مثال دیگری از تکنولوژی‌هایی است که من در مورد آنها ترجیح می‌دهم که از تست‌های کاملاً خالص و ریز استفاده نکنم.

در مورد هزینه TDD چطور؟ آیا توسعه به این روش پرهزینه‌تر است؟ فکر می‌کنم اکثر افراد می‌گویند که دست‌کم در ابتدای کار این گونه است.

قطعاً این هزینه منحنی یادگیری است. شما در ارتباط با بهره‌وری، آن چاله ابتدای کار را خواهید داشت. تعدادی مطالعات تحقیقاتی وجود دارد که تلاش دارند که به‌نوعی اختلاف‌های بین تست‌اول (Test First) و تست‌آخر (Test Last) یا بین تست‌اول و تست‌نکردن (Test Never) را اندازه‌گیری کنند. مشکلی که این‌جا وجود دارد این است که هیچ‌گاه به یک اثبات قطعی نمی‌رسید زیرا باید واقعاً افراد یکسانی را استفاده کنید و بگذارید که برای مدتی بر روی چیزی با یک تکنیک کار کنند و بعد از آن اصولاً آنها را شستشوی مغزی دهید و کاری کنید که هرچه در آن مدت آموخته و یافته‌اند را فراموش کنند و بعد مجبورشان کنید که همان کار را با یک تکنیک دیگر انجام دهند تا بتوانید یک مقایسه معنی‌دار داشته باشید. بنابراین، گروهی از مطالعات یک چیز را پیشنهاد می‌کنند و گروهی دیگر، چیز دیگری را پیشنهاد می‌کنند اما فکر می‌کنم اصولاً اینکه آیا چیزی پرهزینه‌تر یا کم‌هزینه‌تر است و مواردی از این دست، وابسته به باور شخصی شما است. به شخصه، شکی ندارم که برای من بهره‌وری بیشتری دارد که تست‌اول کار کنم تا اینکه تست‌آخر کار کنم.

در ارتباط با تیم‌هایی که دیده‌اید چطور؟ فکر می‌کنید که نسبت به قبل از آنکه TDD را برایشان معرفی کنید، سریع‌تر شده‌اند؟

می‌توانم بگویم که گرایش به سریع‌تر شدن داشتند. مشکلی که اینجا وجود دارد این است که TDD تنها بازیگر ماجرا نیست. در همین حین، در طی زمان، حجم کدتان را افزایش می‌دهید. ممکن است اندازه تیم‌تان را افزایش دهید. ممکن است با یک تیم ۵ نفره شروع کرده باشید اما الان ۲ تیم ۵ نفره باشید یا ممکن است ۱۰ تیم بشوید. بنابراین در طی زمان، متغیرهای دیگر تغییر می‌کنند و سخت است که بگوییم آیا واقعاً سریع‌تر شده‌اند یا خیر زیرا خروجی نهایی مستقیماً وابسته به بهره‌وری اشخاص نیست بلکه متغیرهای زیادی درگیر است. اما به شخصه اعتقاد دارم تیم‌هایی که TDD را به خدمت می‌گیرند سریع‌تر می‌شوند. یعنی اگر خودشان را در حالتی که TDD را به خدمت نگرفته‌اند، تصور کنم و مقایسه کنم. عمده این قضیه به علت داشتن تست است. فکر می‌کنم بزرگترین تفاوت از داشتن تعداد کافی تست ناشی می‌شود که به شما احساس امنیت خوشایندی می‌دهد. به علاوه، از بهبود کیفیت کد و طراحی‌ها نیز سود برده می‌شود و البته خیلی چیزهای دیگر از قبیل یادگیری افراد هم هست.

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

درست است. خیلی این را شنیده‌ام. فکر می‌کنم این یک جنبه اصلی و ذکر نشده در مورد TDD است. کافی نیست که فقط چرخه «تست، کد و بازسازی (Refactor)» را انجام دهید. نیاز دارید که به غیر از آن، حسی در مورد اشیاء در ذهن داشته باشید و لازم است که کدهای تست را هم بازسازی کنید. فقط بازسازی کردن کدهای محصول نیست. شما باید به همان نسبت به کدهای تست‌تان هم اهمیت دهید.

فکر می‌کنید همان اصول راهنما که در مورد کد محصول مفید است در مورد کد تست هم مفید است؟

اعتقاد دارم بله. اما اصل (Principle)، اصل است و با قاعده (Rule) تفاوت دارد. به عنوان مثال ممکن است اصول مختلفی داشته باشید که با هم در تضادند و مجبور شوید که یک تصمیم شخصی بگیرید. مثلاً اینکه امسال یک مقدار کد تکراری اینجا وجود دارد اما این تکرارها، باعث می‌شود که تست‌های کمی ‌بهتر و قابل‌فهم‌تر داشته باشم. یعنی ممکن است تصمیم بگیرید که این کدهای تکراری بماند هرچند که اصول می‌گویند که باید از دستشان خلاص شوید.

و آیا این شرایط را در کد محصول هم می‌یافتید؟

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

بیایید به سراغ موضوعی نه کاملاً متفاوت اما متفاوت برویم. در کتابتان بین TDD و Acceptance TDD تفاوت قائل شده‌اید. به من گفتید که مقداری «توسعه مبتنی بر رفتار» BDD (Behavioral Driven Development) هم انجام داده‌اید. تفاوت بین این سه چیست؟ آیا فقط شکل تکامل‌یافته همدیگر هستند؟ چگونه می‌توانیم مفهوم این سه چیز را ترسیم کنیم؟

من ابتدا در مورد BDD صحبت می‌کنم. منشأ BDD خیلی نزدیک به TDD است. اصطلاح BDD توسط دَن نورث ابداع شد. طبق اظهارات دَن، او این اصطلاح را برای شرایطی به کار گرفت که نمی‌خواست در مورد تست صحبت کند و در عوض می‌خواست در مورد بیان مشخصات برخی رفتارها صحبت کند. پیش از این اشاره کردم که ما از واژه‌ تست به معانی مختلفی استفاده کرده‌ایم. خیلی از افراد از واژه‌ تست، این معنی ذاتیش را درنظر دارند که تست به معنای ارزیابی بعد از کار است. در TDD، معنای آن بیان پیشاپیش مشخصات چیزی است که هنوز وجود ندارد. بنابراین ناهمخوانی وجود دارد. آنچه دَن، سعی در انجام آن داشت (و بعداً به آن BDD اطلاق کرد)، استفاده از واژگان متفاوتی بود که برای طرز فکر TDD مناسب‌تر بودند. BDD از این‌جا آغاز شد.

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

مسیر دیگر تکامل این بود که از تست‌های واحد (Unit Test) سطح پایین خارج شویم. حال چه تست باشد چه بیان مشخصات باشد. تلاش کنیم که از آن خارج شویم و چیزها را از دیدگاه کاربران بیان کنیم و بیشتر در حوزه کاربران نهایی و حوزه واسط کاربری قرار گیریم و به جای زبان‌های برنامه‌نویسی، بیشتر از زبان کاربران استفاده کنیم. در واقع این با انتخاب بین تست یا تست پذیرش (Acceptance Test) رابطه دارد. من فکر می‌کنم این دو جهت اصلی است که BDD تکامل یافته است.

بنابراین فکر می‌کنید که BDD باعث شده است که TDD و Acceptance TDD، تکراری شوند؟

این به نظر درست نمی‌آید. من این‌طور نمی‌گویم. من می‌‌گویم BDD انواع مختلفی قاعده ندارد بلکه تنها یک شاخه از TDD یا تست پذیرش (Acceptance) است.

اما آنطور که من متوجه شدم، BDD نمی‌تواند هر دو جنبه را پوشش دهد.

بله. برای این اصطلاح هم، معانی مختلفی استفاده شده است. انواع مختلفی از BDD وجود دارد. شما « BDD با گرایش کد» و « BDD با گرایش ویژگی‌ها یا عملکردها» را دارید. احتمالاً از ابزارهای مختلفی برای این دو گونه مختلف BDD استفاده می‌کنید. در عمل، شما دو چیز مختلف را اجرا می‌کنید هرچند هر دو BDD خوانده می‌شوند.

آیا برای BDD، ترجیحی در مورد ابزارها دارید؟

من پیش از این به JDave اشاره کردم. من آن را یک فریم ورک تست خواندم اما در واقع یک فریم‌ورک BDD است و مفاهیم و اصطلاحات BDD را درخود تعبیه کرده است. JDave اصولاً الهام‌گرفته از rspec است. امروزه Cucumber یکی از فرزندان پروژه rspec است. Cucumber به نوعی یکی از ابزارهای مورد علاقه من در ارتباط با گونه تست پذیرش از BDD است.

آیا در پروژه‌هایی که شرکت می‌کنید همچنان استفاده از برخی ابزارهای قدیمی تست پذیرش (Acceptance Test) مانند Fitness را در نظر می‌گیرید؟

من زیاد با Fitness کار نکرده‌ام. چند باری به آن برخوردم و به نظرم ابزار خوبی بود. فکر می‌کنم بین این روش مبتنی بر جداول یا حتی نسخه روایتی (Narrative) از Fitness با سناریوها و تست‌هایی که می‌توانیم در ابزارهایی مانند Cucumber داشته باشیم، تفاوت زیادی وجود دارد. تفاوت آن‌چنان کلان نیست اما آن قدر زیاد هست که من دیگر استفاده از ابزاهایی مانند Fitness را در نظر ندارم. البته برای کدهای قدیمی اگر یک سیستم میراث از تست‌های Fitness وجود داشته باشد، احتمالاً همان مسیر را ادامه می‌دهم. اما در یک زمینه خالی، احتمالاً ابزارهایی مانند Cucumber را استفاده می‌کنم.

به من گفتید که دارید کتاب دیگری می‌نویسید.

بله، در واقع در ارتباط با تست واحد (Unit Test) است. انتشاراتم -انتشارات Manning- از من خواست که آیا می‌خواهم با روی آشرو کار کنم. او یک کتاب net. دارد و بیشتر از آنکه جاواکار باشد، اهل net. است. در واقع این همکاری به نوعی ترجمه کتابش به دنیای جاوا است. روشن است که برخی مفاهیم متفاوت هستند و در آن‌جا به کار نمی‌آید بنابراین این‌کار نمی‌تواند یک ترجمه مستقیم باشد.

آیا قرار است فراتر از موضوع مبتنی بر تست (Test Driven) باشد؟

بله، ما به TDD اشاره می‌کنیم و معرفی کوتاهی در مورد آن ارائه می‌کنیم اما اصولاً تمرکز کتاب بر روی هنر تست نوشتن است یا اگر از اصطلاحات BDD استفاده می‌کنید تمرکز بر نوشتن بیان مشخصات است.

آیا کتاب در سطح حرفه‌ای‌ها است؟

بله. به نوعی کتابی برای حرفه‌ای‌ها است. کلی کد منبع و این‌جور چیزها.

چه وقت قرار است منتشر شود؟

گفتنش سخت است. قطعاً می‌توانم بگویم امسال نخواهد بود. امیدوارم یک سال بعد، کتاب در دستم باشد.

در پایان آیا چیزی هست که بخواهید اضافه کنید؟

چیزی به ذهنم نمی‌رسد.

من یک سئوالی به ذهنم رسید. آخرین باری که من شما را دیدم در Agile 2008 بود و شما یکی از ستاره‌های برنامه‌نویسی بودید.آن مسابقات، اساساً یک رقابت در زمینه تست‌کردن به روش TDD در معرض عموم بود. شما مسابقه را بردید. چطور این کار را کردید؟

ما جاسوسیِ جاشوآ -یکی از داوران- را کردیم! علاوه بر آن تقلب هم همراهم برده بودم (شوخی)! فکر می‌کنم ما تکنولوژی‌هایی را انتخاب کردیم که برای حضار، جذاب بود. فکر می‌کنم به مقدار کافی مردم را سرگرم کردیم.