توسعهدهنده نرمافزار
تاریخچه JUnit و آینده تست
مطلبی که میخوانید ترجمه قسمت ۱۶۷ از رادیو مهندسی نرمافزار است. رادیو مهندسی نرمافزار هر یکی دو هفته یک بار مصاحبهای درباره یکی از موضوعات حوزه مهندسی نرمافزار با افراد خبره و با تجربه در موضوع مورد بحث ترتیب میدهد.
در این قسمت با کِنت بِک درباره این چیز کوچکی که خیلی سال پیش ساخته و کار روزانه برنامهنویسانِ بسیار بسیار زیادی را تغییر داده صحبت میکنیم: تست خودکار واحد (Unit Testing) و JUnit. به شکل مختصر تاریخچه JUnit را مرور میکنیم و دراینباره صحبت میکنیم که چطور آغاز شد و بعد از آن چه رخ داد. ما درباره توسعه مبتنی بر تست (Test Driven Development) یا به اختصار TDD بحث خواهیم کرد و دراینباره صحبت میکنیم که چه زمانی باید TDD کرد و چه زمانی نباید کرد و درباره تجربههای دنیای وحشی گپ میزنیم. این قسمت با برخی دیدگاههای شخصی در مورد آینده تست و به شکل عمومیتر مهندسی نرمافزار پایان مییابد.
خوش آمدی کِنت!
خیلی ممنونم. خوشوقتم که اینجا هستم.
خیلی خرسندیم که با شما مصاحبه میکنیم. لطفاً خود را برای شنوندگان معرفی کنید و کمی در مورد گذشته و پیشزمینه خود برایمان بگویید.
من یک خوره نسل سومی هستم. پدربزرگم یک خوره رادیو بود. پدرم یک خوره الکترونیک بود که خوره برنامهنویسی شد. حالا من پیشه خانوادگیمان را در پیش گرفتهام. من در محیطی آکنده از تکنولوژی بزرگ شدم. وقتی ۱۲-۱۳ ساله یا شاید ۱۱ ساله بودم، برنامهنویسی را آغاز کردم؛ روی ماشین حسابی که پدرم به خانه آورده بود برنامهنویسی میکردم.
نمیدانم چه چیزهایی را برجسته کنم که برای شنوندگان جذاب باشد! خیلی کارها کردهام. الگوهای نرمافزاری مبحثی بود که در ابتدا بر روی آن کار کردم. [کارهای دیگری هم کردهام مثلاً] متدولوژی Extreme Programming و توسعه مبتنی بر تست (Test Driven Development)، معماری xUnit که اولین بار برای زبان SmallTalk پیادهسازی شد و بعداً بهوسیله اریک گاما و من به زبان جاوا ترجمه شد. غیر از آن مثلاً بر روی طراحی واکنشی (Responsive Design) هم کار کردهام. به چه چیز دیگری علاقهمندید؟!
شما کتابهای زیادی نوشتهاید. این خیلی خوب است.
بله، فکر میکنم تا الان ۸ کتاب نوشته باشم.
فکر میکنم به مقیاس جامعه ما زیاد باشد.
بله، با استاندارد خورهها، زیاد است.
فکر میکنم در توییتر خواندم که از هواداران قسمتهای رادیو مهندسی نرمافزار هستید. درسته؟
بله، من در ۸ هکتار از زمینهای چمنزار و جنگلی زندگی میکنم که در خارج از یک دهکده کوچک قرار دارد که آن هم خارج از یک شهر کمی بزرگتر است؛ در وسط ناکجا آباد! به همین خاطر، تفریحات زیادی دارم. من به پادکستهای زیادی گوش میدهم از جمله فکر میکنم به همه قسمتهای رادیو مهندسی نرمافزار گوش دادهام. خیلی خوب است که موقعی که در حال بیل زدن، کودپاشی و غذا دادن به حیوانات هستید، به چیزی گوش دهید که غذای ذهنتان باشد.
جالب است. خیلی باعث افتخار است که شنونده همه قسمتها بودهاید. لطفاً کمی در مورد تاریخچه JUnit برایمان بگویید. کار چطور آغاز شد؟
بله، اولین کار تجاری من در زبان SmallTalk بود. SmallTalk یک تاریخچهای از تست داشت اما تست خودکار نبود. تغییرات کمی در برنامهتان میدادید و مدتی میگذشت و تغییرات بیشتری میدادید. بنابراین، این چرخههای افزایشی (Incremental) شکل گرفته بود اما تست خودکار نبود. من زنجیرهای از کارها را تجربه کردم و ۴-۵ تکنیک مختلف برای نوشتن تستهای خودکار را آزمودم. بعد، یک روز از من خواسته شد که به یک تیم توسعه، مشاوره بدهم.
اوایلِ کار مشاورهام بود. من میدانستم که میخواهم به آنها بگویم که تست خودکار بنویسند اما روشی برایشان نداشتم که چطور این کار را بکنند. در همان حالی که نوعی اضطراب و هراس داشتم گفتم به من اجازه بدهید که از همان ساختاری که برای تست در محیط SmallTalk داریم استفاده کنم. ما متغیرهایی داریم که مقداردهی اولیه میشوند و بعد هم عبارات را داریم. گفتم که چطور است که آنها را بیهیچ تکلفی، به مدل اشیاء برگردانیم. محیط کاری شما معادل میشود با یک کلاس. متغیرهایی که استفاده میکنید تبدیل میشود به نمونههای متغیر (Instance Variable) و تکه کدی که برای تست مینویسید، متدهای کلاس میشوند. این، نوعی نگاشت بیتکلف بود. من گفتم بهجای اینکه تست را دستی انجام دهیم و ببینیم که چه چیزی در خروجی چاپ میشود و آن وقت مطمئن شویم که تست موفق بوده، بگذاریم کامپیوتر این کار را بکند. از همین جا بود که اعلانها (Assertion) آمدند. منشأ معماری از اینجا بود و این مربوط به سال ۱۹۹۲ میشود. این اولین فریمورک تست واحد بود که همه این موفقیتها از آنجا ناشی شد.
چند سال بعد، وقتی در زوریخ زندگی میکردم، با اریک گاما هر ازچند گاهی برنامهنویسی میکردم. در آن زمان جاوا کاملاً جدید بود. من میخواستم آن را یاد بگیرم. یک بار من و اریک با هواپیما داشتیم برای کنفرانس OOPSLA میرفتیم. او از من شنیده بود که در مورد این معماری تست صحبت کرده بودم و من هم صحبتهای او را شنیده بودم که جاوا خیلی جالب است. ما همانجا نشستیم و اولین نسخه JUnit را با روش تست اول، نوشتیم که این خودش یک مسأله جالب خودراهاندازی بود! چگونه میتوان تستی نوشت که یک فریمورک تست را تست کند؟ مشکلات مختلفی خواهد داشت. شما میدانید که ما این گونه چالشهای فکری را دوست داریم.
وقتی از هواپیما پیاده میشدیم اولین نسخه JUnit را نوشته بودیم. منظورم این است که چیزی که نوشتیم خیلی کوچک بود. JUnit چیز خیلی کوچکی است. آیا با این وجود میتوانست مفید باشد؟ وقتی رسیدیم آنجا در آتلانتا آن را به مارتین فاولر دادیم. او خیلی سریع امتحانش کرد و گفت چیز خیلی خوبی است. ما آن را به چند نفر دیگر هم دادیم و بازخوردهای واقعاً خوبی گرفتیم. این گونه بود که کار آغاز شد.
و واقعاً موفق بوده است. امروزه، JUnit تبدیل به یک استاندارد نوشتن تست خودکار برای جاوا شده است. درست است؟
بله.
بعد از آن چه شد؟ این مربوط به خیلی وقت پیش میشود. بعد از آن، چه بر سر JUnit آمد؟
بله، سال ۱۹۹۷ بود که نسخه اول را نوشتیم. یک تغییر بزرگ معماری که بعدها اتفاق افتاد، تغییر از سبک فریمورک که در آن فرزندانی برای کلاسهای موجود مینویسید و برخی متدها را بازنویسی (Override) میکنید به سبک DSL بود که در جاوا با حاشیهگذاری (Annotation) پیادهسازی میکنید. من NUnit را در همان اوایل کار -وقتی فقط چند فایل بر روی سیستم جیمز نیوکِرک بود- دیدیم و آنجا حاشیهگذاریها (Annotation) را دیدم و همان موقع گفتم این ایده خیلی جالبی است.
بعد از آن متوجه روشهای دیگری شدم مثلاً اینکه متدها مجبور باشند که با عبارت test آغاز شوند که این هم خود نوعی فراداده (Metadata) است زیرا ما آن را تحلیل میکنیم. شما میدانید که JUnit یک زبان است که نحو خاص خودش را دارد که با نحو زبان جاوا متفاوت است. بهعنوان مثال ارثبری به روش متفاوتی از جاوا، انجام میشود. ما این زبان را در جاوا ساختهایم اما واقعاً جاوا نیست و تحلیل میشود. اما کامپایلر جاوا میتواند در مورد حاشیهگذاریها کمک کند. اگر کسی تستش را با عبارت test آغاز کند، این معنی خاصی نمیدهد و باعث نمیشود که آن تست اجرا شود اما با استفاده از حاشیهگذاریها میتوانید این کار را بهدرستی انجام دهید.
ترجمههای زیادی از فریمورک JUnit برای زبانهای دیگر وجود دارد. نظر شما در مورد این جنبش چیست؟
در چندین سطح میتوانم آن را تعبیر کنم. یک سطح این است که یک طراحی خوب بوده است. منظورم این است که یک چیز مرسوم برای تست است. شما میخواهید که دستههای مجزا داشته باشید، بتوانید آنها را بدون اینکه با هم تداخل کنند، با هم ترکیب کنید و میخواهید همه این کارها را در همان زبان کد برنامه انجام دهید. اینکه تست شما در همان زبان کد برنامه نباشد مزایای زیادی دارد اما در همین حال، اینکه مانند JUnit، تست شما در زبان کد برنامه باشد نیز مزایای زیاد خود را داراست.
الان چه کسی دارد روی نسخه جاری کار میکند؟ شما همچنان کار میکنید. درسته؟
بله، هر هفته با دیوید سَف که الان در گوگل هست، جمع میشویم و چند ساعت روی آن کار میکنیم.
شما الان، برای نسخه بعدی برنامهریزی کردهاید؟
بله، داریم روی آن کار میکنیم البته ما یک تیم جدید بزرگی نیستیم.
نسخه آخر یک ویژگی خیلی جالب ارائه کرد که راه خودش را در بین کاربران پیدا کرد. مزیت اینکه یک پروژه را برای ۱۳ سال اداره کنید این است که ارزش بردباری را درمییابید. اگر یک ویژگی جدید ابداع کنید که فکر میکنید خارقالعاده است، در صورتی که ۵ سال بعد، افراد در حال استفاده از آن باشند، آن وقت میتوان گفت که بله، واقعاً ویژگی جالبی بوده است. اما اگر ۳ سال گذشته و افراد هنوز از آن استفاده نمیکنند، هنوز نمیتوان فهمید آیا ویژگی جالبی است یا نه.
اما ویژگی جالبی که الان بحثش را کردم، چیزی است که ما به آن قاعده (Rule) میگوییم. اگر بخواهم ساده توضیحش دهم باید بگویم وقتی تست اجرا میشود، زنجیرهای از اشیاء ایجاد میشوند. یک شیء متد Before را اجرا میکند، یک شیء دیگر Timeout را Catch میکند، شیء دیگری خطا را گزارش میکند و ... . حال با استفاده از Rule، شما میتوانید اشیائی به این زنجیره اضافه کنید. (قاعدهها) به نوعی اشیاء فراداده برای تست هستند. این چیز مهمی است. بهجای اینکه برای اشتراکات تستها از ارثبری استفاده کنید، از ترکیب کردن اشیاء (Composition) استفاده میکنید. این روش، منعطفتر است. افرادی که بهدنبال یک ویژگی جالب در JUnit هستند، این چیزی است که باید یک نگاهی به آن بکنند.
فکر میکنید این یک ویژگی کلیدی برای امکان استفاده مجدد (Reuse) در کدهای تست است؟
نمیدانم که ویژگی کلیدی است یا نه. ما ۱۳ سال بدون آن سپری کردهایم! ولی فکر میکنم خیلی مفید است. من کوشیدهام تا نوعی عبارات بیانی (Declarative) برای تست داشته باشیم. اینکه تست را بخوانید و برایتان یک داستان بگوید. قاعدهها روشی برای آماده کردن صحنه تست است، روشی که واضح باشد و در عین حال کاملاً ساده باشد. مثلاً شما میتوانید قاعدهای داشته باشید که یک پوشه (Folder) موقتی بسازد و همه کارهایی که در هنگام تست با فایلها انجام میدهید، بعد از آن، بهطور خودکار پاک شود. بنابراین وقتی دارید تست را میخوانید، میگویید: «ببین، اینجا یک پوشه موقتی ساخته شده است، مطمئنم که قرار است کار با فایل در اینجا انجام شود.» بنابراین قاعدهها، گرچه در یک زبان دستوری (Imperative) قرار میگیرند اما یک حس بیانی (Declarative) ایجاد میکنند که فکر میکنم برای تست مفید است.
بنابراین فقط نوعی ابزار برای استفاده مجدد (Reuse) نیست بلکه برای گویاتر کردن هم هست. درسته؟
بله. چون هر تستی باید داستانی برایتان بگوید. باید ابتدا، وسط و انتها داشته باشد. باید نوعی جریان و فراز و نشیب داشته باشد. فراتر ار اینها، باید مقصود داشته باشد؛ باید بتوانید از تست مقصودش را بخوانید. قاعدهها روش دیگری برای مهیا کردن زمینه تست است. چیزی که از همه قبلیها کمی قویتر است.
و در کدام نسخه معرفی شد؟ نسخه ۴ ممیز چند؟
فکر میکنم ۴ ممیز ۸. میتوانید آن را در GitHub بیابید، جایی که ما الان کدها را نگهداری میکنیم البته بعد از مدت طولانی و موفقیتآمیزی که در SourceForge نگهداری میشد.
شما اصطلاح توسعه مبتنی بر تست (Test Driven Development) را ابداع کردید. آیا میتوانید یک آشنایی مختصر بدهید که توسعه مبتنی بر تست چیست؟
بله، این یک ایده مضحک است. همیشه بهترین ایدهها، مضحک هستند. یعنی اگر ایده مضحکی داشته باشید که کار کند، حتماً چیز ارزشمندی است. اگر ایده خوبی داشته باشید که کار کند اهمیتی ندارد زیرا حتماً کس دیگری به آن فکر کرده است اما اگر ایده مضحکی داشته باشید که کار کند، میتوانید بهوسیله آن برتری یابید. [توسعه مبتنی بر تست،] ایده مضحکی است که میگوید وقتی میخواهید کد بنویسید، اول تستی بنویسید که رَد میشود و فقط وقتی تست قبول میشود که کدی که تصورش را کردهاید واقعاً موجود باشد. یعنی جریان کار برنامهنویسی را برعکس میکند.
فکر میکنید وقتی توسعه مبتنی بر تست میکنید، مهمترین جنبهاش چیست؟ اینکه چون اول تستها را مینویسید ابتدا به طراحی فکر میکنید و پیادهسازی برآمده از دیدگاه کاربر کلاس است؟ یا چیز دیگری است؟
چیزی که بعد از چندین سال توسعه مبتنی بر تست، در ذهن من رخ میدهد، این است که ابتدا یک عملکردی را تصور میکنم و فکر بعدی من این است که چگونه آن را تست کنم. بعد از اینکه یک عملکردی را تصور میکنم، هیچگاه فکر نمیکنم که مثلاً باید از فلان کلاس ارث ببرم و آن یکی را استخراج کنم و دیگری را آنجا بگذارم و .... . در مورد این چیزها که همهشان مربوط به پیادهسازی هستند فکر نمیکنم بلکه بعد از تصور یک عملکرد، گام بعدی من این است که به این فکر میکنم که چطور میخواهم آن را تست کنم.
برای من کار بزرگی است که بخواهم در مورد نحوه پیادهسازی یک چیزی فکر کنم و در همان حال، در مورد این فکر کنم که -چون ممکن است اشتباه کرده باشم- چطور میخواهم از درست بودن آن مطمئن شوم. گاهی برایم خیلی بزرگ است که همهاش را در ذهنم بیاورم. اگر بتوانم آن را به دو بخش مجزا کنم و ابتدا این را رسیدگی کنم که چطور میخواهم آن را تست کنم، آن وقت یک کار سادهتر و کماسترستری دارم که تمرکز کردن بر روی آن راحتتر است.
اگر نمیتوانید برای چیزی تست بنویسید، در واقع موضوع برنامهنویسی ندارید. این برای اغلب برنامهها درست است. البته وقتی موضوع کارم خیلی اکتشافی باشد، در مورد این چیزها فکر نمیکنم یعنی اگر بخواهم بفهمم که آیا کلاً میتوانم برنامهای بنویسم که فلان کار را انجام دهد، آن موقع، کد را مینویسم و دستی آن را تست میکنم. بعد وقتی فهمیدم بله، ممکن است؛ آنگاه روش را عوض میکنم و کمی عقب رفته و در مورد نحوه تستش فکر میکنم.
خیلی از افرادی که من با آنها صحبت کردهام هنوز هم فکر میکنند که تست کردن قبل از نوشتن خود کد، واقعاً کار احمقانهای است. بعضی وقتها از انجام آن میترسند. مشاهدات شما از تطابق افراد با توسعه مبتنی بر تست در عمل چیست؟
من کنجکاوم بدانم چه مشکلی پیش میآید. مغزتان منفجر نمیشود! بگذارید از شما بپرسم آیا توسعه مبتنی بر تست کردهاید؟
بله.
پس خودتان وقتی کسی به شما چنین چیزی بگوید، چه میگویید؟
من میگویم به من کمک میکند که در گامهای کوچک رو به جلو حرکت کنم. گامهای پایداری برایم فراهم میکند. کمکم میکند که به این فکر کنم که چه کاری لازم است انجام دهم و چطور باید آن را پیادهسازی کنم.
بله، این همان حس و تجربه من هم هست. من فکر میکنم برخی موانع، اجتماعی هستند. وقتی وارد رشته کامپیوتر میشوید میبینید دانشجویان با رتبه آ، برنامهنویس میشوند و دانشجویان با رتبه ب، تست میکنند. بخشی از آنچه باید برآن فائق آیم، این نوعِ دید اجتماعی است. اینکه (کسی فکر کند) من برنامهنویسم، مجبور نیستم تست بنویسم! که البته درست نیست اما داریم در مورد ذهن بشر صحبت میکنیم. البته من در مورد افرادی که شما درموردشان صحبت میکنید چیزی نمیدانم اما برای من چنین مانعی وجود داشته است.
من باید بهشکل مشخص ۳۰ تا ۴۰ درصد از زمان خود را به نوشتن تست اختصاص دهم اما وقتی تست مینویسم فقط خود تست نوشتن نیست؛ در آن زمان من در حال تصمیمات API و تحلیل هم هستم و تستها، روشی است که برای ثبت این تصمیمات بهکار میگیرم و سرانجام تعدادی تست هم خواهم داشت.
فکر میکنید توانستهاید اکثر افرادی که با آنها صحبت کردهاید را قانع کنید؟
نمیدانم. من دیگر تلاش نمیکنم که افراد را قانع کنم. فکر میکنم واقعاً ۱۰ سال را اینطور سپری کردم که تلاش میکردم افراد را برای TDD و برنامهنویسی زوج (Pair Programming) و انواع باید و نبایدها قانع کنم اما دیگر این کار را نمیکنم. من همواره کار میکنم تا تجربیاتم را بهبود دهم. من مشتاقم آنچه میآموزم را به اشتراک بگذارم و همین طور تلاش میکنم که به آنچه دیگران در کارهایشان تجربه کردهاند گوش دهم و فرا بگیرم. من پیگیر این نیستم که چه تعداد افرادی TDD انجام میدهند. فکر میکنم اگر رویهمرفته، روش توسعه نرمافزار بهبود یافته باشد، خیلی خوب است و اگر من هم تأثیر کوچکی در آن داشته باشم، آن هم خیلی خوب است.
شما سئوالی پرسیدید که میزان انتشار TDD بین افراد چقدر است. من فکر میکنم هنوز درصد خیلی کمی از افراد هستند که واقعاً اینطور کار کنند که هیچ خط کدی ننویسند مگر آنکه قبل از آن یک تست رَد شده برایش نوشته باشند. اما فکر میکنم افراد خیلی زیادی هستند که تست و ارزش بالقوه آن برایشان آشکار شده است.
اما هنوز پیش میآید که به جایی میروم و میگویند که ما یک مقداری تست کردیم اما کارمان را متوقف میکرد و آن را رها کردیم. این برای من خیلی عجیب است. به نظرم ارسطو هم شگفتزده میشد، منطق این کار جور در نمیآید. منطق تست این است که اگر تست کار میکند پس برنامهام هم کار میکند و اگر تست کار نمیکند پس برنامهام هم کار نمیکند. اگر وقتی تست کار نمیکند، عمل بعدی شما این باشد که تست را پاک میکنید و گزارش تست را نادیده میگیرید، این به آن معناست که برنامهتان کار نمیکند. بنابراین نتیجهای که از این بحث میتوانم داشته باشم این است که بهغیر از کار کردن برنامه، فشارهای زیاد دیگری هم بر روی افراد هست اما خیلی بد است زیرا فکر میکنم یک سود بالقوهای وجود دارد که افراد بهعنوان برنامهنویس در صورتی میتوانند آن را داشته باشند که به تست کردن اعتماد کنند و بیشتر به آن توجه کنند.
در مورد تست، فلسفههای دیگری هم وجود دارد مثلاً برخی بجای TDD، توسعه مبتنی بر رفتار (Behavioral Driven Development) را عنوان میکنند. نظر شما در این باره چیست؟
فکر میکنم از جنبه کلی، این خیلی مثبت است که برنامهنویسان فعالانه، مسئولیت کیفیت کارشان را میپذیرند. این چیز خوبی است و اینکه چه عنوانی به آن میدهید، مسئله ثانوی است. یکی از چیزهایی که در همان اولین توضیحاتم در مورد TDD، روی آن بحث میکردم اهمیت تست کردن در سطوح مختلف بود. بنابراین اینطور نیست که TDD، همان فلسفه تست یونیت باشد. من در هر سطحی که به پیشرفت گام بعدی کارم کمک کند تست مینویسم. بعضی مواقع ممکن است برخی افراد آنها را تست عملکردی (Functional) بنامند. بهعنوان مثال، ۴۰٪ از تستهای JUnit از طریق API عمومی کار میکنند و ۶۰٪ از آنها بر روی اشیاء سطح پایینتر کار میکنند. API عمومی چیز خوبی برای تست کردن است. اینکه شما باید این سهم ۴۰ و ۶۰ را داشته باشید یا سهم ۱۰ و ۹۰ یا اینکه ۹۰ و ۱۰ باشد را من واقعاً نمیدانم. فقط میخواهم این ایده را مطرح کنم که بخشی از TDD این است که بتوانید بین سطوح مختلف جابجا شوید.
مثلاً وقتی مشتری میگوید که فلان سناریو باید مقدار ۵ برگرداند، شما یک تست مینویسید. همین که چنین سناریویی باید مقدار ۵ برگرداند و وقتی برنامهتان را تست میکنید و عمیقتر میشوید میبینید که بله، یک شیءای که مقادیر ورودی ۵ و ۷ را میگرفته باید ۵ برمیگردانده است. این جا موقعیت مناسبی برای نوشتن تست [بعدی] است زیرا این تکه دیگری از داستان است که باید بازگو شود. اما آیا اینکار توسعه مبتنی بر تستهای پذیرش (Acceptance Test) است؟ یا BDD است؟ فکر میکنم اینکه بین سبکهای مختلف، دیوارهای محکمی بنا کنیم، اشتباه است. من بهعنوان برنامهنویس نیاز دارم که همه این سطوح را بشناسم. تست کمکم میکند که آنها را بفهمم و بههمین دلیل در همه سطوح تست مینویسم.
این ما را تا حدودی به بحث مقیاسهای بزرگ میکشاند. من پروژههایی را دیدهام که هزاران تست واحد (Unit Test) دارند و با این حجم عظیم تستهای واحد مشکلاتی دارند. نظر شما چیست؟ آیا تست واحد واقعاً مقیاسپذیر است؟ شما در سطوح کوچک (و نه در سطح تستهای پذیرش)، هزاران کلاس دارید و حجم عظیمی از تستهای واحد وجود خواهد داشت که گاهی ۲-۳ برابر کدهای اصلی میشوند. تجربه شما دراینباره چیست؟
به نظر من اگر ۲-۳ برابر کد، تست وجود دارد کدهای محصول حتماً خیلی پیچیده هستند اما داشتن این مقدار تست کاملاً طبیعی است. یکی از اشخاصی که از تستهایی که میکرد، بیشترین چیز را یاد گرفتم، یک کامپایلرنویس بود. او برای هر خطی از کد کامپایلر ۵ خط تست نوشته بود. او یکی از موثرترین افرادی است که تا بهحال دیدهام بنابراین این واقعاً برایم الهامبخش بود. اما البته داخل کامپایلر بهشدت پیچیده است بنابراین اینکه نسبت ۱ به ۱ باشد یا ۱ به ۵ باشد، من را نگران نمیکند.
اما شما همچنان فکر میکنید که TDD یا تست خودکار را برای سطوح مختلف داشته باشید. هم تستهای خیلی کوچک برای واحدهای خیلی کوچک و هم چیزهای بزرگتر و هم چیزهای در سطح تست پذیرش.
دیوید سَف که الان در JUnit مشارکت دارد، مدعی است که تستها تمایل دارند که یا به سمت کوچکترین سطح اشیاء مهاجرت کنند و یا به سمت بزرگترین سطح اشیاء. او داشت در مورد تست برای نرمافزارهای Eclipse صحبت میکرد [که این را مطرح کرد]. من نمیتوانم این را بپذیرم. من در تجربیاتم متوجه چنین چیزی نشدهام. من تستهایی در همه سطوح میانی داشتهام که از داشتن آنها خوشحال بودهام و هزینه زیادی برایم نداشته است.
شما الان گفتید که اگر فقط بخواهید بفهمید که آیا پیادهسازی یک چیزی ممکن هست یا خیر، تستی برایش نمینویسید. آیا موارد دیگری هم هست که فکر میکنید برای آنها TDD نکنید؟ من یک توییت در توییتر را بهخاطر میآورم که میگفت توسعهدهندگان خوب میدانند که چه موقع تست کنند و چه زمان تست نکنند. آیا ممکن است یک کم در این مورد توضیح دهید.
تست هزینهها و منافع خودش را دارد. برخی از منافعش کوتاهمدت هستند و برخی بلندمدت هستند. همینطور برخی از هزینهها کوتاه مدت و برخی دیگر بلندمدت هستند. و شرایط مختلف، مقاطع کوتاه مدت و بلند مدت متفاوتی دارند. بهعنوان مثال، یک زمان من یک سایت تمرین پوکر راهاندازی کردم. زیرا من داشتم پوکر بازی کردن را یاد میگرفتم و در انجام تطبیق الگو (Pattern Matching) مشکلاتی داشتم. مثلاً اینکه اگر ۷ کارت داشته باشیم چطور تشخیص دهم دست من چیست؟ آیا یک زوج دارم یا دو زوج دارم یا ...؟ اول میخواستم بدانم اگر نرمافزاری برای این کار داشته باشم که قدرت تشخیصمان را بالا ببرد، چیز جالبی خواهد شد؟ من نسخه اول نرمافزار را به زبان SmallTalk نوشتم و بعد آن را به Java Script ترجمه کردم. من تستی نداشتم اما متوجه شدم که بله، چیز جالبی میشود. بعد خواستم که آن را به شرایط بیشتری گسترش داده و مدل حوزهاش (Domain Model) را غنیتر کنم. وقتی این کار را کردم، آن موقع شروع به نوشتن تست کردم. وقتی تست نوشتن را آغاز کردم متوجه شدم که تکه کار خیلی بزرگ است و بخشی را خارج کردم و توانستم تستها را راحتتر بنویسم. اما یک بازه انتقال وجود داشت. در ابتدا، نگران این بودم که آیا کار واقعاً مفید خواهد بود یا خیر و البته جواب میتوانست خیر باشد بنابراین منافع بلندمدت اینکه با دقت برایش تست بنویسم رفته بود. وقتی فهمیدم که چیز خوبی است و من میخواهم آن را داشته باشم و برای مدتی از آن نگهداری کنم، در آن زمان منافع بلندمدت مجدداً وارد شده و احتمال بیشتری یافتند طوری که من میتوانستم آنها را ببینم؛ بنابراین روش را عوض کردم.
اما در مجموع آنچه من به آن فکر میکنم این است که هزینهها و منافع، در کوتاهمدت و در بلندمدت چه هستند؟ وقتی JUnitMax که یک پلاگین Eclipse بود را شروع کردم، نوشتن تست برای پلاگین Eclipse سخت نبود -میدانید که من با اریک یک کتاب در مورد آن نوشتهام- با این وجود برخی مواقع، با بعضی چالشهای واقعی مواجه میشدم زیرا API به شدت قدرتمند و منعطف بود اما آنچنان برای تست کردنِ مجزا تنظیم نشده بود. بنابراین وقتی JUnitMax را آغاز کردم، تست خودکار نداشتم. این به آن خاطر نبود که ندانم آیا قرار است برای مدت طولانی این کد را پشتیبانی کنم زیرا من آن موقع میدانستم که ایده JUnitMax خیلی خوب است و حتی اگر هیچ کس دیگر آن را نخواهد من برای خودم آن را میخواهم و نگهداریاش میکنم اما دلیل آن این بود که هزینه نوشتن چنان تستهایی خیلی زیاد بود خصوصاً در اولین ماهی که JUnitMax را مینوشتم. آن موقع تست خودکار نداشتم و دستی تست میکردم اما زمانی رسید که پیچیدگی کد به حدی رسید که من نگران این موضوع شدم که اگر بخواهم چیزی را تغییر دهم، ممکن است چیز دیگری را خراب کند. در آن زمان، تست نوشتن را آغاز کردم. هماکنون برای JUnitMax به مقدار کافی تست دارم. در واقع، بهعلت اینکه هزینههای کوتاهمدت خیلی زیاد بود، با وجود اینکه میدانستم منافع بلندمدتی خواهم داشت، برایش تست خودکار ننوشته بودم و در اینجا نیز وقتی متوجه شدم که تعادل میان اینها (هزینهها و منافع)، عوض شده است، روشم را تغییر دادم یعنی گفتم ویژگی قبلی را بدون تست خودکار انجام دادم اما برای این ویژگی میخواهم تست خودکار داشته باشم حتی اگر قرار باشد هزینه قابل ملاحظهای برای تستش داشته باشم و با این کار خوشحالم. فکر میکنم به این روش فکر میکنم.
این خیلی جالب است زیرا من افرادی را میشناسم که نسبت به تست واحد و TDD خیلی بیشتر تعصب دارند و میگویند قبل از پیادهسازی هر خط کد از هرچیزی، تست بنویسید زیرا موجب میشود در مورد طراحی تکه کدتان فکر کنید و ... . آنها از شما خیلی متعصبتر هستند. دیدن این ایدههای مختلف جالب است.
به نظر من میارزد که به تعصب، بهعنوان یک ابزار یادگیری بنگریم. (تجربه کنم که) اگر بگویم همواره برای همه چیز تست مینویسم چه اتفاقی میافتد؟ و بعد ببینم که چه خوب بوده که این کار را کردهام یا اینکه متأسف شدهام که این کار را کردهام. [ببینم] چقدر شده که با خود گفتهام ایکاش تست واحد نمینوشتم و چقدر از نوشتن تستها خوشحال شدهام و از اینها برای پیشرفت کارم بهره ببرم.
برخی در وبلاگهایشان مینویسند یا میگویند که: «آیا امروزه بیش از حد تست نمینویسیم؟» نظر شما چیست؟
بگذار این به یک مشکل بزرگ تبدیل شود بعد تلاش کنیم که آن را برطرف کنیم!
:-) باشه!
منظورم این است که بهنوعی یکی از خِرسهای همان داستان سه خِرس است. حتماً «خیلی کم» وجود دارد، شاید «خیلی زیاد» هم وجود داشته باشد و جایی هم در میانه آنها هست که دقیقاً درست است. آیا داستان سه خرس را میدانید؟
فکر نکنم.
یک دختر کوچولو داخل خانه خرسها میشود. از غذای پدر خانه میخورد که زیادی داغ است، از غذای مادر خانه میخورد که زیادی سرد است، در نهایت از غذای بچه خرس میخورد که مناسب است. وقتی این داستان را تعریف میکنم منظورم این است که بله، میتوانید خیلی زیاد داشته باشید، میتوانید خیلی کم داشته باشید و جایی در میانه آن درست است اما من فکر نمیکنم افراد واقعاً بدانند «خیلی زیاد» واقعاً چیست. من میخواهم چنین چیزی را تجربه کنم که هم تستها و هم فعالیتهای تیم، آنقدر بشود که حس کنم خیلی زیاد است اما با وجود اینکه تلاش کردهام هیچگاه به چنین چیزی نرسیدهام!
بسیار خوب. چه چیزی باعث میشود یک تست یا تست واحد خوب شود و چه چیزی باعث میشود بد باشد؟ من مطمئنم که شما تعداد زیادی از هر دو مورد تستهای خوب و بد را دیدهاید.
من فکر میکنم هر تستی باید داستانی را روایت کند که اگر کسی بعداً بیاید و آن را بخواند باید چیزی مهم در مورد برنامه بفهمد. بنابراین اولین صافی من، گویایی آن برای افراد است. اگر بخواهیم به زبان پزشکی بگوییم، تستها باید تشخیص تفاضلی (Differential Diagnosis) داشته باشند. مثلاً وقتی تست خون میدهید بر اساس نتیجه آن، میتوانید برخی چیزها را از تشخیصتان خارج سازید و برخی چیزهای دیگر را تأیید کنید. هر تستی باید بتواند برنامه خوب را از بد، مجزا سازد. اگر تستی داشته باشید که پیشرفتی در جهت فهم برنامه خوب از بد ایجاد نکند، احتمالاً تست خوبی نیست. اگر فضای کلیه برنامههایی که تلاش در حل مسألهتان دارند را درنظر بگیرید، تعداد خیلی اندکی میتوانند این کار را بکنند و بقیه نمیتوانند. یک تست باید بخش بزرگی از آن فضا را هرس کند. هر برنامهای که این تست را ارضاء نکند، قطعاً مسأله اصلی را نمیتواند حل کند.
مورد دیگر افزونگیها (Redundancy) است. اگر تعدادی تست داشته باشید که دقیقاً چیز یکسانی را بگویند، من میگردم که ببینم کدامیک از آنها کمترین ارزش افزوده را دارد و آن را پاک میکنم البته باید دقیقاً درباره چیزی باشد که قبلاً پوشش داده شده باشد.
آیا فکر میکنید قواعد طراحی که همه ما در مورد برنامهنویسی عادی میشناسیم، برای کدهای تست هم معنی میدهند؟ مثلاً ارث بردن؟
در مورد خاص ارثبری، من برای کدهای تست، موافقش نیستم زیرا دوست دارم تستهایم مانند یک داستان خوانده شوند. یکی از امکانات JUnit این است که میتوانید یک کلاس والد راهانداز (Setup) داشته باشید که قبل از همه تستها اجرا شود. به این ترتیب میتوانید کدهای مشترک مربوط به راهاندازی تستهای مختلف در کلاسهای مختلف را استخراج کنید. اگر این کار (ارثبری) را برای ۳ یا ۴ سطح انجام دهید در آن صورت، تست خیلی بیشاخ و برگ میشود. تست فقط میگوید که یک شیء یا پیغام داریم و این مقدار باید برگشت داده شود اما اگر بخواهید بفهمید واقعاً چه اتفاقی دارد میافتد و داستان را بفهمید، باید به کلاس و Setup آن و کلاس والد و Setup آن و کلاس والد والد و Setup آن و ... نگاه کنید، این کار زمان میبرد. این روش مانند این است که یک لطیفه را بدون اینکه مقدماتش را بگوییم، نقل کنیم که مزهای ندارد.
آیا قاعده واقعاً عجیب دیگری در مورد تست مشاهده کردهاید؟
بله، وجود دارد. یکی از آنها که واقعاً عجیب است اما خیلی مفید است و میتوانم توضیح بدهم این است که تست بهصورت تصادفی، رد نمیشود. اگر یک تست یک بار رد شود، احتمال اینکه همان تست بهزودی بازهم رد شود، نسبت به جمعیت کلیه تستها، بیشتر است. بیشتر تستها این طورند که وقتی قبول میشوند دیگر رد نمیشوند. این مبتنی بر مشاهدات صدها میلیون اجرای تست از توسعه ۵۰ توسعهدهنده در طول یک سال است. اگر یک تست شروع کند به کار کردن، (به احتمال زیاد) همچنان قبول خواهد شد اما اگر یک تستی رد شود احتمال اینکه در اجرای بعدی هم رد شود خیلی بیشتر است. ما از این موضوع در JUnitMax که یک پلاگین تست برای جاوا در Eclipse بود استفاده کردیم تا تستها را اولویتبندی کنیم و تستی که بیشترین احتمال رد شدن را دارد را ابتدا اجرا کنیم. توجه ما براین موضوع بوده که از تستی که اجرا میکنید بیشترین بازخورد را بگیرید. زیرا اگر واقعاً بتوانید -و من فرض میکنم با استفاده از تحلیلهای آماری بتوانید- اثبات کنید که اگر تستی آخرین بار قبول شده، این بار هم قبول خواهد شد دیگر نیازی به اجرای آن نخواهید داشت. این قاعده آخرین رد شدهها، اکتشافی است که با هزینه خیلی کم میتوان آن را محاسبه نمود زیرا فقط کافیست مقدار کمی حافظه در مورد رخدادهای گذشته، داشته باشید و با استفاده از آن میتوانید تستها را اولویتبندی کنید؛ تستهای جدید و تستهایی که اخیراً رد شدهاند ابتدا اجرا میشوند.
مورد دیگر این است که اگر هر مجموعه تستی را در نظر بگیرید، میبینید که اجرای برخی تستها زمان کمتری میبرد و اجرای برخی دیگر طولانیتر است. این زمانهای اجرا، توزیع توانی (Power Law) دارند. به این ترتیب که تعداد خیلی خیلی زیادی تست دارید که خیلی سریع اجرا میشوند و تعداد اندکی تست دارید که خیلی طول میکشد تا اجرا شوند. اگر نمودار هیستوگرام آنها را بکشید میتوانید این آمار مضحک را ببینید. من نمیتوانم توضیح دهم که چرا این درست است اما این را آنقدر در جاهای مختلف دیدهام که باور دارم کاملاً عمومی است.
در این صورت آیا شما این دو مجموعه را از هم مجزا میکنید و مثلاً تستهای طولانیمدت را با نرخ کمتر و یا در بیلدهای شبانه اجرا میکنید؟
آنچه این توزیع توانی میگوید این است که بیشتر تستها میتوانند در یک زمان کوتاه اجرا شوند بنابراین میتوانید به این شکل آنها را از هم مجزا کنید. در واقع وقتی JUnitMax را اجرا میکنید، تستهای کوتاهمدتتر را اول اجرا میکند. آنچه من هنوز تایید نکردهام و خیلی دوست دارم دادههای بیشتری در مورد آن داشته باشیم این است که آیا تستهای طولانیمدتتر، احتمال رد شدن بیشتری دارند یا خیر. بدترین نتیجه در صورتی خواهد بود که هرچه تستها زمان بیشتری بگیرند، اطلاعات بیشتری هم تولید کنند اما اگر همبستگی بین آنها وجود نداشته باشد، بهترین حالت است. در آن صورت، با اجرا کردن تستهای کوتاهمدتتر، بیشترین اطمینان را از برنامهتان حاصل میکنید.
ممکن است کمی بیشتر در مورد JUnitMax توضیح دهید. شما چند بار به آن اشاره کردید، ممکن است به ما بگویید دقیقاً چه چیزی است؟
حتماً. JUnitMax، یک تستکننده مستمر برای Eclipse است. [با استفاده از آن،] اگر پروژهای را در Eclipse اجرا کنید میبینید که همه تستها بهصورت خودکار اجرا میشوند. دقیقاً مشابه کامپایلر است؛ در Eclipse ابزار مجزایی برای کامپایل ندارید که بهسراغش بروید و اجرایش کنید بلکه اگر تغییری در کد بدهید، کامپایلر بهصورت خودکار اجرا میشود و بازخورد میدهد، با بکارگیری JUnitMax، تستها هم همینگونه خواهند شد. یک ابزار تست مجزا نخواهید داشت، یک پنجره مجزایی نخواهید داشت که بهسراغش بروید بلکه بازخوردها را همان موقع میگیرید.
هرچه من بیشتر TDD انجام میدادم، بیشتر تستها را اجرا میکردم. اگر فرض کنیم که اجرای تستها، ۱۰ ثانیه طول بکشد، اگر آنها را در هرساعت، ۲۰ بار اجرا کنید - که احتمالاً یک تخمین محتاطانه است- و هربار که تستها را اجرا میکنید منتظر پاسخش بمانید، با این فرضها، میتوانید محاسبه کنید که چه هزینهای فقط برای انتظار کامل شدن اجرای تستها میپردازید. من میخواستم این هزینه را از میان ببرم. برای این منظور از حقه اولویتبندی تست و یکپارچه کردن اجرای تست با محیط استفاده کردم. رَد شدن تستها درست مانند خطاهای کامپایل، نمایش داده میشود. برای دریافت بازخوردها لازم نیست، به جای دیگری بروید. بنابراین فقط لازم خواهد بود چند ثانیه صبر کنید تا با خود بگویید همینقدر کافی است و به کد زدن بازمیگردم. هر زمان که من خواستم این اعداد را محاسبه کنم، مضحک به نظر آمد بنابراین بهعهده خوانندگان میگذارم که حساب کنند چه مقدار زمان انتظار برای تستها صرف میکنند و اگر نتایج تست را در ۱۰٪ آن زمان داشته باشند چه مقدار صرفهجویی مالی خواهند داشت.
بنابراین بخشی از انگیزه من از آنجا ناشی میشد که این برایم ناامیدکننده بود که بخشی از زمانم را در انتظار پاسخ تستها صرف میکردم و بخش دیگر انگیزه من از آنجا بود که میخواستم زندگی برنامهنویسی خودم را داشته باشم. من میتوانم به اینجا و آنجا مسافرت کنم و سخنرانی کنم و مشاوره بدهم و ... اما این نوعی زندگی خوشگذرانی است. من ترجیح میدهم این کار را نکنم. من عاشق برنامهنویسی هستم. این راهی بود که میتوانستم زندگی برنامهنویسی خودم را بسازم. بنابراین ترکیبی از این دو انگیزه بود: یکی کمتر منتظر شدن برای تستها و دیگری بهعنوان روشی برای پرداخت هزینههای خودم، خانوادهام، شهریه دانشگاه و ... از طریق فعالیتی که هنوز هم واقعاً عاشقش هستم و آن JUnitMax بود.
جالب است. آیا لازم است برای آن تستهای خاصی بنویسیم یا همان تستهای عادی نوشته شده برای مثلاً نسخه ۲ یا ۳ از JUnit را میتوان بهوسیله آن اجرا نمود؟
شما میتوانید اینطور به آن نگاه کنید که یک جایگزین برای همان اجراکننده تست JUnit است که با Eclipse همراه است.
بنابراین لازم نیست هیچ کد خاصی در برنامهام برای آن داشته باشم.
نه، بههیچ وجه. فقط نتایج تست را با انتظار کمتری برایتان فراهم میکند.
از کجا میتوانم آن را بگیرم؟
از JUnitMax.com
از com. به نظر میرسد که متنباز نباشد و باید بابتش پول بدهیم.
بله، ۱۰۰ دلار برای هر سال. هرباری که محاسبه کردهام دیدهام هر ۲ روز یکبار هزینهاش را برگردانده است.
بسیار خوب. ما خیلی در مورد تاریخچه تست و تست در گذشته صحبت کردیم. بیایید نگاهی به آینده بیاندازیم.
فکر میکنم JUnitMax نوعی از باورهای من در مورد مسیر آینده است؛ جایی که تست به میزان کامپایلر برای برنامهنویسی اهمیت مییابد؛ وقتی که تست، هر دقیقه برایتان منفعت میآورد پس شما هم سرمایهگذاری بیشتری روی تولید آنها میکنید. فکر میکنم گرایش فراوانی در زمینه طراحی برای تستپذیری (Design for Testability) شکل خواهد گرفت. مثلاً سیستمی مانند Eclipse طراحی زیبایی دارد اما سخت است که برایش تستهای موجزی بنویسیم که سریع اجرا شوند. قطعاً راههای طراحی دیگری هم وجود دارد که بتوانیم بهسادگی تستهایی بنویسم که بهسرعت اجرا شوند و از شکست به علت عوامل خارجی در امان باشند.
برای اینکه واقعاً چطور این کار را خواهید کرد، فکر میکنم خیلی چیزها باید فراگرفته شود. اما وقتی ارزش تست کردن افزایش مییابد، همزمان ارزش فراگیری این مهارتهای طراحی هم افزایش خواهد یافت. در نتیجه، این چرخه شکل خواهد گرفت که «تست بیشتر»، انگیزهای میشود که نیاز به «طراحی تست پذیرتر» داشته باشیم که آن نیز خود منجر به این میشود که تست کمهزینهتر و باارزشتر شود که آن هم خود دوباره منجر به «تست بیشتر» میشود. و اینکه تستها با تکرار بیشتری اجرا خواهند شد؛ بیشتر تستها در اغلب زمانها اجرا میشوند. این مرحله بعدی با چیزهایی مانند JUnitMax خواهد بود. اگر بخواهید در هر ثانیه از تجربه برنامهنویسی، بازخوردهای بیشتری داشته باشید، باید تستها بهصورت موازی با کار اجرا شوند.
چیزی مانده که بخواهید با شنوندگان ما در میان بگذارید؟ یک توصیه خردمندانه؟
توصیه خردمندانه من این است که به توصیههای خردمندانه اعتماد نکنید. من به نتیجهگیریهای اخلاقی که افراد در پایان داستانهایشان میکنند اعتمادی ندارم. من خیلی دوست دارم که به داستانهایشان گوش دهم اما وقتی میخواهند نتیجهگیری کنند که بنابراین شما باید درست مانند من این کارها را بکنید، فکر میکنم خیلی از شرایط زمینهای را نادیده میگیرند. بنابراین «به داستانها گوش دهید و داستانها را تعریف کنید.» این به نوعی نتیجهگیری اخلاقی من است و چون یک داستان نیست میتوانید نادیده بگیریدش! :-)
یک سئوال شاید سخت میخواهم بپرسم. فکر میکنید مهندسی نرمافزار در ۵ سال آینده، به چه شکل خواهد بود؟
گرایش زیادی به مستقر کردن (Deployment) بیشتر وجود دارد که همه چیزهای دیگر از آن نشأت میگیرند. همه تغییرات اجتماعی، تغییرات فنی، تغییرات در زبانها و رویهها، تغییرات در زیرساختها و همه تغییراتی که لازم است رخ دهند از اینجا ناشی میشوند. اگر قرار باشد هر روزه ۵۰ بار (محصول را) مستقر کنید پایگاه داده و ابزارهای استقرارتان باید تغییر کنند. جداسازی میان واحدهای کاری دیگر و کار توسعه باید از میان برود. بازاریابی، فروش، مدل تجاری و همه چیزهای دیگر وقتی خیلی خیلی زیاد استقرار داشته باشید، تغییر خواهند کرد. این گرایشی است که من توقفی برای آن نمیبینم.
بسیار خوب. شاید ما باید قسمتی را به صحبت در مورد استقرار مستمر (Continuous Deployment) بپردازیم.
من خیلی دوست دارم.
خیلی خوب. بسیار ممنونم که به این مصاحبه آمدید.
مطلبی دیگر از این انتشارات
مستندسازی چابک
مطلبی دیگر از این انتشارات
ریزسرویسها
مطلبی دیگر از این انتشارات
توسعه نرمافزار به روش تَرکهای (Lean)