30 روز با TDD: روز چهارم - Pass کردن اولین تست

داستان چیه؟

سپتامبر سال گذشته آقای James Bender در وبلا‌گ‌های تلریک یک مجموعه نوشته منتشر کرد به نام 30 روز با TDD. من می‌خوام یک ترجمه آزاد از این نوشته‌ها براتون داشته باشم تا با هم درباره Test Driven Development بیشتر بدونیم. اگر نمی‌دونید داستان TDD چی هست نگران نباشید، این مجموعه نوشته‌ها برای همین هست که با این روش بیشتر آشنا بشیم ;)

#30RoozTDD

سایر نوشته‌های مربوط به این سری رو در صفحه "30 روز با TDD" می‌تونید ببینید. در توییتر هم با هشتگ 30RoozTDD# می‌تونید مطالب مرتبط به این سری نوشته‌ها رو پیدا کنید. حرکت 30 روز با TDD برای ارتقاء دانش خودم و همه دوستان و همکاران برنامه‌نویسم هست و اگر کسی مایل هست از این حرکت حمایتی بکنه، می‌تونه با من در تماس باشه.

و اما چهارمین روز: کاری کنیم که اولین تست Pass شود

نوشته زبان انگلیسی را در این آدرس می‌توانید مشاهده کنید. در روز سوم، اولین تست‌مان را نوشتیم. هدف این بود: 

"یک متد بنویسید که یک جمله و یک کاراکتر را به عنوان ورودی دریافت کند و عددی را برگرداند که مشخص کند آن کاراکتر چند بار در آن جمله استفاده شده است"

کدی در پایان نوشته روز سوم، داشتیم کد زیر بود. در این کد فقط متد مربوط به تست را نوشتیم و امروز کاری خواهیم کرد که این تست Pass شود.

       
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ThirtyDaysOfTDD.UnitTests
{
    [TestFixture]
    public class StringUtilsTests
    {
        [Test]
        public void ShouldBeAbleToCountNumberOfLettersInSimpleSentence()
        {
            var sentenceToScan = "TDD is awesome!";
            var characterToScanFor = "e";
            var expectedResult = 2;
            var stringUtils = new StringUtils();
            int result = stringUtils.FindNumberOfOccurences(sentenceToScan, characterToScanFor);
            Assert.AreEqual(expectedResult, result);
        }
    }
}

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

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

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

قبل از اجرای تست لازم است که solution را کامپایل کنم و اینجاست که بلافاصله اولین خطا را می‌توان دید:

 بر اساس تعریف اگر solution شما در Visual Studio کامپایل نشود، تست شما fail‌ محسوب می‌شود. در این مثال، ریشه خطا برمی‌گردد به کلاس StringUtils که در خط 19 کد تلاش کردم یک instance از آن ایجاد کنم در حالی که نمونه‌ای از آن در کد وجود ندارد.

حالا برمی‌گردیم به مسیر TDD، جایی که باید کلاس‌هایی که در حال تست‌شان هستیم (در مورد این مثال کلاس StringUtils) را بسازیم. اینجا دو گزینه پیش‌رو داریم: یا باید کلاس را داخل همین پروژه فعلی بسازیم (بعضی برنامه‌نویس‌ها حتی آن را داخل فایل کلاس تست می‌سازند) یا یک پروژه Class Library جدید ایجاد و در آن کلاس مورد نظر را بسازیم. 

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

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

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

برای ایجاد یک کلاس جدید روی پروژه کلیک راست کرده و از منوی Add گزینه کلاس را انتخاب کنید و نام StringUtils را به آن بدهید. کلاس را public کنید. تا کدی مشابه کد زیر داشته باشید:

       
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ThirtyDaysOfTDD.UnitTests
{
    public class StringUtils
    {
    }
}

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


بر اساس خطا، کار بعدی که باید انجام دهم اضافه کردن تعریف متد FindNumberOfOccurneces  به کلاس StringUtils است. این کار را انجام می‌دهم اما در داخل بدنه متد، کد خاصی نمی‌نویسم. نتیجه به صورت زیر خواهد بود:

       
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ThirtyDaysOfTDD.UnitTests
{
    public class StringUtils
    {
        public int FindNumberOfOccurences(string sentenceToScan, string characterToScanFor)
        {
            // TODO: Implement this method
            throw new NotImplementedException();
        }
    }
}

پیشنهاد مترجم: ابزارهای مختلفی برای اجرا تست‌ها وجود دارند، اگر به دنبال ابزار رایگان هستید، استفاده از نسخه رایگان TestDriven.NET را پیشنهاد می‌کنم. TestDriven.NET از NUnit پشتیبانی می‌کند و بعد از نصب، برای اجرا تست‌ها کافی است روی پروژه کلیک راست کرده و گزینه Run Tests را انتخاب کنید. اگر به دنبال ابزارهای غیررایگان هستید Resharper به گمان من بهترین باشد. شما هم می‌توانید ابزارهای مورد نظر خود را در بخش نظرات معرفی کنید. 

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

خب چه اتفاقی افتاد؟ تست fail شد. چرا که ما در متد FindNumberOfOccurences هیچ کدی ننوشته یک exception را ایجاد کردیم. بسته به ابزاری که برای اجرا کردن تست‌ها انتخاب می‌کنید،‌ می‌توانید خطای مربوط به این exception را مشاهده کنید.

قدم بعدی این است که ساده‌تری راه را برای اینکه تست Pass شود انتخاب و در کد پیاده‌سازی کنیم. هدف تست ما این است که در جمله "!TDD is awesome" تعداد کاراکترهای "e" را پیدا کنیم و خروجی مورد انتظار ما 2 است، پس اجازه بدهید کد را به صورت زیر بازنویسی کنیم: 

       
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ThirtyDaysOfTDD.UnitTests
{
    public class StringUtils
    {
        public int FindNumberOfOccurences(string sentenceToScan, string characterToScanFor)
        {
           return 2;
        }
    }
}

در کد بالا خیلی راحت عدد 2 را return می‌کنیم. حالا دوباره تست را اجرا کنید. نتیجه؟ تست Pass‌ می‌شود، هورا! نرم‌افزار آماده عرضه است!

خب، واضح است که برنامه ما هنوز قابل عرضه نیست. منظور ما از پیاده‌سازی قابلیت مورد نظر، برگرداندن عدد دو به صورت hardcode شده در کد نبود. اما اگر تستی که نوشتیم، تنها تست موجود برای این قابلیت بود، می‌توانستیم بگوییم کار تمام شده است و مهمتر اینکه دیگر develop بیشتری در این کد انجام نمی‌دادیم. به عنوان برنامه‌نویس گاهی وقت‌ها سخت است که متوجه شویم چه زمانی دیگر کافی است: بعضی اوقات قابلیت‌هایی در کد اضافه می‌کنیم که در لیست مشخصات نرم‌افزار نیست چرا که فکر می‌کنیم "آن‌ها الان نمی‌دانند، ولی بعداً به این نیاز خواهند داشت"

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

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

برگردیم به مثال خودمان. در مثال ما، می‌توان گفت که test case موجود کافی نیست. برای بهبود کیفیت کد، ما یک تست جدید اضافه می‌کنیم: 

من می‌خواهم جمله "Once is unique, twice is a coincidence, three times is a pattern" را به همراه کاراکتر "n" به متد پاس بدهم و خروجی مورد انتظار هم عدد 5 است.

گام بعدی نوشتن یک تست جدید است. اسم متد تست را ShouldBeAbleToCountNumberOfLettersInAComplexSentence می‌گذارم و آن را به کلاس StringUtilsTest به شکل زیر اضافه می‌کنم: 

       
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ThirtyDaysOfTDD.UnitTests
{
    [TestFixture]
    public class StringUtilsTests
    {
        [Test]
        public void ShouldBeAbleToCountNumberOfLettersInSimpleSentence()
        {
            var sentenceToScan = "TDD is awesome!";
            var characterToScanFor = "e";
            var expectedResult = 2;
            var stringUtils = new StringUtils();
            int result = stringUtils.FindNumberOfOccurences(sentenceToScan, characterToScanFor);
            Assert.AreEqual(expectedResult, result);
        }

        [Test]
        public void ShouldBeAbleToCountNumberOfLettersInAComplexSentance()
        {
            var sentenceToScan = "Once is unique, twice is a coincidence, three times is a pattern.";
            var characterToScanFor = "n";
            var expectedResult = 5;
            var stringUtils = new StringUtils();

            int result = stringUtils.FindNumberOfOccurences(sentenceToScan, characterToScanFor);

            Assert.AreEqual(expectedResult, result);
        }
    }
}

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

آخرین باری که تست fail شده داشتیم برای زمانی بودن که متد کلاسی که در حال تست آن بودیم پیاده‌سازی (implement) نشده بود. این بار متد، پیاده‌سازی شده است، اما نتیجه‌ای که می‌خواهیم (عدد 5) را نمی‌گیریم. بسته به اجرا کننده تست،‌ خطای Expected: 5 But was: 2 را خواهید دید.

ساده‌ترین راه برای اینکه تست قبلی Pass شود این بود که خیلی راحت عدد دو را برگردانیم. این بار فکر می‌کنم ساده‌ترین راه این باشد که یک الگوریتم که تعداد یک کاراکتر را داخل یک جمله پیدا کند را پیاده‌سازی کنیم. این الگوریتم را در متد FindNumberOfOccurences در کلاس StringUtils به شکل زیر پیاده‌سازی می‌کنیم:

       
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ThirtyDaysOfTDD.UnitTests
{
    public class StringUtils
    {
        public int FindNumberOfOccurences(string sentenceToScan, string characterToScanFor)
        {
            var stringToCheckAsCharacterArray = sentenceToScan.ToCharArray();
            var characterToCheckFor = Char.Parse(characterToScanFor);
            var numberOfOccurenes = 0;
            for (var charIdx = 0; charIdx < stringToCheckAsCharacterArray.GetUpperBound(0); charIdx++)
            {
                if (stringToCheckAsCharacterArray[charIdx] == characterToCheckFor)
                {
                    numberOfOccurenes++;
                }
            }
            return numberOfOccurenes;
        }
    }
}


این ممکن است بهینه‌ترین یا بهترین راه حل مساله نباشد، ولی الان مهم نیست: این ساده‌ترین است و من دنبال ساده‌ترین راه برای Pass کردن تست‌هایم می‌گردم. اگر دوباره تست‌ها را اجرا کنیم می‌بینیم که الگوریتم، پاسخگویی نیازمندی ما بوده است و بنابراین هر دو تست Pass می‌شوند.

در آینده دوباره این مثال را با هم بررسی می‌کنیم تا درباره چند مبحث پیشرفته TDD و Refactoring با هم صحبت کنیم. ولی الان من می‌دانم که صرفنظر از اینکه بعداً با کد چه کار خواهم کرد، تا زمانی که این دو تست Pass شوند، کد من همچنان پاسخگوی این دو test case خواهد بود.

ادامه دارد...

به اشتراک گذاری این نوشته در شبکه‌های اجتماعی

30 روز با TDD: روز سوم - اولین تست شما

داستان چیه؟

سپتامبر سال گذشته آقای James Bender در وبلا‌گ‌های تلریک یک مجموعه نوشته منتشر کرد به نام 30 روز با TDD. من می‌خوام یک ترجمه آزاد از این نوشته‌ها براتون داشته باشم تا با هم درباره Test Driven Development بیشتر بدونیم. اگر نمی‌دونید داستان TDD چی هست نگران نباشید، این مجموعه نوشته‌ها برای همین هست که با این روش بیشتر آشنا بشیم ;)

#30RoozTDD

سایر نوشته‌های مربوط به این سری رو در صفحه "30 روز با TDD" می‌تونید ببینید. در توییتر هم با هشتگ 30RoozTDD# می‌تونید مطالب مرتبط به این سری نوشته‌ها رو پیدا کنید. حرکت 30 روز با TDD برای ارتقاء دانش خودم و همه دوستان و همکاران برنامه‌نویسم هست و اگر کسی مایل هست از این حرکت حمایتی بکنه، می‌تونه با من در تماس باشه.

و اما سومین روز: نوشتن اولین تست TDD

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

اولین و مهترین ابزاری که به آن نیاز داریم Visual Studio هست. من از نسخه 2013 ویژوال استودیو برای مثال‌های این سری نوشته استفاده می‌کنم. اگر شما از نسخه 2012 هم استفاده می‌کنید مشکلی نخواهید داشت. در این سری نوشته‌ها از Nuget استفاده زیادی خواهیم کرد،‌ بنابراین اگر از نسخه 2008 ویژوال استودیو استفاده می‌کنید، این نوشته را برای راهنمایی درباره اضافه کردن امکان Nuget به VS 2008 مطالعه کنید. اگر ویژوال استدیو ندارید، می‌توانید نسخه اکسپرس رایگان را از مایکروسافت دانلود کنید.

ساده‌ترین راه برای نوشتن یک آزمون واحد (Unit Test) با #C یا VB.NET استفاده از framework‌های آزمون واحد است. تعدادی framework برای آزمون واحد در NET. وجود دارد اما برای این سری نوشته‌ها من از NUnit‌ استفاده خواهم کرد که تنها framework موجود برای Unit Test در NET. نیست اما شاید محبوب‌ترین باشد.

در نهایت به یک اجراکننده تست (test runner) احتیاج داریم. test runner نرم‌افزاری است که اسمبلی‌های کامپایل شده unit test‌های ما را مورد بررسی قرار می‌دهد، به دنبال متدهایی که به عنوان test method‌ علامت‌گذاری شده باشند می‌گردد، آن متدها را اجرا و نتیجه را گزارش می‌کند. 
اگر NUnit را دانلود و نصب کنید، به test runner مربوط به NUnit هم دسترسی خواهید داشت که یک برنامه خارج از NET. برای اجرای تست‌های NUnit است. تعدادی add-in برای ویژوال استودیو وجود دارد که تست‌ها را از داخل خود ویژوال استودیو اجرا می‌کند و ما هم در این سری نوشته‌ها از یکی از این ابزارها استفاده خواهیم کرد. (توضیح مترجم: نویسنده اصلی قصد دارد از یکی از ابزارهای پولی شرکت تلریک به نام JustCode استفاده کند، اما ما از ابزارهای رایگان استفاده خواهیم کرد)

آماده سازی صحنه 

هدف ما در این مثال این است:

"یک متد بنویسید که یک جمله و یک کاراکتر را به عنوان ورودی دریافت کند و عددی را برگرداند که مشخص کند آن کاراکتر چند بار در آن جمله استفاده شده است"

به اندازه کافی ساده به نظر می‌رسد. می‌دانیم ورودی و خروجی مورد انتظار و کاری که لازم است انجام بدهیم چیست. حالا که ابزارهای مورد نیاز را داریم و نیازمندی را هم مشخص کردیم، می‌توانیم به ویژوال استودیو وارد شده و کار را شروع کنیم.

ابتدا ویژوال استودیو را باز کرده و از منو File گزینه New و بعد Project را انتخاب می‌کنیم. در پنجره باز شده به بخش Other Project Types رفته و مطابق شکل زیر Blank Solution را انتخاب می‌کنیم.

بعد از انتخاب Blank Solution یک نام به solution می‌دهیم. من ThirtyDaysOfTDD را انتخاب کردم (شما می‌توانید نام دیگری را انتخاب کنید) و بعد هم روی Ok کلیک می‌کنیم. حالا مطابق شکل زیر یک solution‌ خالی در ویژوال استودیو خواهیم داشت.

 

سپس باید یک پروژه برای نگهداری unit test هایم ایجاد کنم. شما باید unit test هایتان را در یک پروژه جدا از کد اصلی برنامه نگهداری کنید چرا که قرار نیست تست‌ها را هم همراه برنامه deploy کنید. به علاوه این کار باعث مرتب بودن solution شما می‌شود، همچنین من integration test ها را هم در پروژه جدا از unit test ها قرار خواهم داد.

برای ایجاد یک پروژه برای unit test من در بخش Solution Explorer ویژوال استودیو روی ThirtyDaysOfTDD کلیک راست می‌کنم و از منوی ظاهر شده گزینه Add و بعد New Project را انتخاب می‌کنم و در پنجره باز شده مانند شکل زیر برای زبان #C یک Class Library به نام ThirtyDaysOfTDD.UnitTests ایجاد می‌کنم.


محل ایجاد پروژه را همان محل پیش‌فرض در نظر می‌گیرم و Ok‌ می‌کنم. حالا باید مطابق شکل زیر پروژه برای من ایجاد شده باشد.

در پروژه ایجاد شده خود ویژوال استودیو یک کلاس به نام Class1 برای ما ایجاد کرده است. از همین کلاس استفاده خواهیم کرد اما قبل از آن نامش را به StringUtilsTests تغییر می‌دهیم.

بعد از این کار نوبت به اضافه کردن NUnit به پروژه می‌رسد. می‌توانید NUnit را مستقیماً دانلود و نصب کنید و بعد به اسمبلی‌های NUnit در پروژه به صورت دستی reference بدهید، اما اگر با ویژوال استودیو 2012 به بالا کار می‌کنید می‌توانید از Nuget برای اضافه کردن NUnit به پروژه استفاده کنید. 
برای گرفتن NUnit با استفاده از Nuget من به مسیر زیر در منو Solution‌ می‌روم: Tools –> Library Package Manager –> Manage NuGet Packages. با کلیک بر روی Manage Nuget Packages پنجره Nuget باز می‌شود. در بخش Online مطابق شکل زیر دنبال Nunit بگردید (کادر جستجو در بالای سمت راست است و با وارد کردن کلمه مورد نظر و زدن Enter می‌توانید جستجو کنید) بعد از چند ثانبه مطابق شکل زیر می‌توانید NUnit را در نتایج جستجو مشاهده کنید.

ممکن است در نتایج جستجو، مانند شکل بالا موارد زیادی را ببینید، مورد مشخص شده را انتخاب و Install کنید. در این مرحله پنجره انتخاب پروژه باز می‌شود که در آن مطابق شکل زیر تنها پروژه موجود را انتخاب کنید

حالا اگر به بخش References پروژه خود بروید مطابق شکل زیر اضافه شدن اسمبلی NUnit به پروژه را می‌توانید مشاهده کنید

 

حالا نوبت به کدنویسی می‌رسد :)

نوشتن اولین تست

حالا که یک پروژه داریم که اسمبلی NUnit هم به آن اضافه شده می‌توانم اولین تستم را بنویسم. اما قبل از نوشتن تست، باید به NUnit بگویم که کلاس StringUtilsTests حاوی Unit test است، این کار با افزون خاصیت (Attribute) به نام TestFixture به شکل زیر انجام می‌شود:

 using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ThirtyDaysOfTDD.UnitTests
{
    [TestFixture]
    public class StringUtilsTests
    {
    }
}

توجه داشته باشید که TextFixture بخشی از کتابخانه NUnit است، بنابراین حتماً به using ابتدای کد توجه کنید. بر اساس نیازمندی که ابتدای مثال مطرح کردیم، من می‌توانم خیلی راحت، تست ساده‌ای برای سناریویی که همه ورودی‌ها در محدوده قابل قبول قرار دارند بنویسم. برای این حالت، من تستم را این‌طور تعریف می‌کنم:

من می‌خواهم جمله "!TDD is awesome" و کاراکتر "e" را پاس بدهم و خروجی باید عدد 2 باشد (یعنی کاراکتر e دو بار در آن جمله تکرار شده است) حالا باید یک متد بنویسم که کاراکتر این تست را انجام دهد.

من اسم متد را ShouldBeAbleToCountNumberOfLettersInSimpleSentence را می‌گذارم که هیچ پارامتر ورودی دریافت نمی‌کند و خروجی هم ندارد (void‌ است) شاید اسم متد به نظر طولانی برسد ولی مشکلی نیست. توجه داشته باشید که اسم متد باید تا حد امکان تستی که می‌خواهد انجام دهد را مشخص کند. البته این کار همیشه ساده نیست، اما باید تا جایی که می‌توانید به یک اسم خوب نزدیک شوید. 

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

قدم بعدی این است که این متد ShouldBeAbleToCountNumberOfLettersInSimpleSentence به عنوان یک متد تست در NUnit‌ معرفی کنیم که این کار با استفاده از خاصیت (Attribute) به نام Test انجام می‌شود. کد زیر را ببینید:

 using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ThirtyDaysOfTDD.UnitTests
{
    [TestFixture]
    public class StringUtilsTests
    {
        [Test]
        public void ShouldBeAbleToCountNumberOfLettersInSimpleSentence()
        {
            
        }
    }
}

Arrange, Act, Assert

الگوها (Patterns) در توسعه نرم‌افزار خیلی به درد بخور هستند. آن‌ها روشی برای تبدیل مشکلات پیچیده به گام‌های ساده محسوب می‌شوند. بهترین الگوها، آن‌هایی هستند که وقتی به استفاده‌شان عادت کنید، حس طبیعی داشته باشند. وقتی نوبت به unit test می‌رسد الگوی Arrange, Act, Assert یا AAA تبدیل به یک استاندارد منطقی می‌شود.

AAA سه مرحله‌ای که همه unit test ها دارند را پوشش می‌دهد. در Arrange تست را با استفاده از ورودی (input) و خروجی مورد انتظار آماده سازی می‌کنیم. در تست اولی که می‌خواهم بنویسم این کار را با معرفی رشته ورودی نمونه و کاراکتری که دنبالش می‌گردم و همچنین خروجی مورد انتظار (عدد 2) به شکل زیر انجام می‌دهم:

        [Test]
        public void ShouldBeAbleToCountNumberOfLettersInSimpleSentence()
        {
            var sentenceToScan = "TDD is awesome!";
            var characterToScanFor = "e";
            var expectedResult = 2;
        }

علاوه بر تعریف ورودی و خروجی مورد انتظار، باید یک وهله (instance) از کلاسی که می‌خواهم تستش کنم هم ایجاد کنم. در اینجا اسم کلاس را StringUtils گذاشته‌ایم، دقت کنید که این کلاس هنوز وجود ندارد و به تعریف آن در قسمت بعدی این سری نوشته خواهیم پرداخت. کد تابع ShouldBeAbleToCountNumberOfLettersInSimpleSentence تا به حال این‌طور خواهد بود:

        [Test]
        public void ShouldBeAbleToCountNumberOfLettersInSimpleSentence()
        {
            var sentenceToScan = "TDD is awesome!";
            var characterToScanFor = "e";
            var expectedResult = 2;
            var stringUtils = new StringUtils();
        }

مرحله بعد در AAA بخش Act است که جایی است که متد لازم برای تست کلاس فراخوانی شده و نتیجه آن دریافت می‌شود. من اسم این متد را FindNumberOfOccurences گذاشتم. دقت کنید که نه کلاس StringUtils‌ و نه متد FindNumberOfOccurences هنوز نوشته نشده‌اند و ما باید بعداً آن‌ها را ایجاد کنیم.

        [Test]
        public void ShouldBeAbleToCountNumberOfLettersInSimpleSentence()
        {
            var sentenceToScan = "TDD is awesome!";
            var characterToScanFor = "e";
            var expectedResult = 2;
            var stringUtils = new StringUtils();
            int result = stringUtils.FindNumberOfOccurences(sentenceToScan, characterToScanFor);
        }

و در نهایت به بخش Assert می‌رسیم. جایی که بررسی می‌کنیم که آیا نتیجه‌ای که از تست متد کلاس StringUtils بدست آمد با نتیجه مورد انتظار ما مطابقت دارد یا خیر؟ این کار را با استفاده از Assert انجام می‌دهیم. کلاس Assert یک کلاس static در کتابخانه NUnit است که شامل متدهایی برای بررسی و مقایسه نتایج و اعلام pass یا fail یا بی‌نتیجه (inconclusive) بودن تست است. برای این تست من از AreEqual استفاده می‌کنم که دو مقدار را با هم مقایسه می‌کند. اگر دو مقدار با هم برابر باشند، تست ادامه پیدا می‌کند. به این ترتیب امکان استفاده از چند Assert در یک متد تست وجود دارد. 

اگر اجرای متد تست بدون پیغام خطا به پایان برسد، NUnit تست را pass‌ شده فرض می‌کند. متدهای دیگری هم در کلاس Assert وجود دارند که در نوشته‌های بعدی درباره‌شان صحبت خواهم کرد.

در حال حاضر کد نهایی تست مثال این نوشته به شکل زیر خواهد بود:

        [Test]
        public void ShouldBeAbleToCountNumberOfLettersInSimpleSentence()
        {
            var sentenceToScan = "TDD is awesome!";
            var characterToScanFor = "e";
            var expectedResult = 2;
            var stringUtils = new StringUtils();
            int result = stringUtils.FindNumberOfOccurences(sentenceToScan, characterToScanFor);
            Assert.AreEqual(expectedResult, result);
        }

در قسمت بعدی، کاری خواهیم کرد که تست pass شود. در واقع کدهای مربوط به کلاس StringUtils را تکمیل خواهیم کرد.

ادامه دارد...

به اشتراک گذاری این نوشته در شبکه‌های اجتماعی

30 روز با TDD: روز دوم‌ - مروری بر اصول شی‌گرایی

داستان چیه؟

سپتامبر سال گذشته آقای James Bender در وبلا‌گ‌های تلریک یک مجموعه نوشته منتشر کرد به نام 30 روز با TDD. من می‌خوام یک ترجمه آزاد از این نوشته‌ها براتون داشته باشم تا با هم درباره Test Driven Development بیشتر بدونیم. اگر نمی‌دونید داستان TDD چی هست نگران نباشید، این مجموعه نوشته‌ها برای همین هست که با این روش بیشتر آشنا بشیم ;)

#30RoozTDD

سایر نوشته‌های مربوط به این سری رو در صفحه "30 روز با TDD" می‌تونید ببینید. در توییتر هم با هشتگ 30RoozTDD# می‌تونید مطالب مرتبط به این سری نوشته‌ها رو پیدا کنید. حرکت 30 روز با TDD برای ارتقاء دانش خودم و همه دوستان و همکاران برنامه‌نویسم هست و اگر کسی مایل هست از این حرکت حمایتی بکنه، می‌تونه با من در تماس باشه.


و اما
دومین روز: مروری بر اصول شی‌گرایی

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

در نوشته امروز مرور مختصری خواهیم داشت بر اصول برنامه‌نویسی شی‌گرا (OOP یا Object Oriented Programming). حتی اگر برنامه‌نویس با سابقه و با تجربه‌ای هستید باز هم توصیه می‌کنم حداقل بخش‌های مربوط به Polymorphism و Interface ها را مطالعه کنید چرا که این مباحث معمولاً خیلی اشتباه فهمیده می‌شوند و البته بخش مهمی از کار TDD را تشکیل می‌دهند.

OOP چیست؟

Object Oriented Programming یا OOP یک روش برنامه‌نویسی است که در آن برنامه‌نویس از اشیا و ایده‌های جهان واقعی مدلی انتزاعی (abstract) در کد ایجاد می‌کند. وقتی با OOP برنامه‌ای ایجاد می‌کنید، کلاس‌هایی می‌نویسید که اشیا دنیای واقعی را مدل می‌کنند. در برنامه شما از این کلاس‌ها، نمونه‌ها (وهله یا instance) در قالب اشیاء (Objects) ایجاد می‌شوند و در طول کار برنامه این متدهایی بر روی این اشیاء فراخوانی شده یا به عنوان پارامتر به متدهای دیگر پاس داده می‌شوند.

عبارات class و object را نمی‌توان به جای هم به کار برد. کلاس تعریفی از این است که شی باید چطوری باشد. در واقع کد #C یا VB شما نقشه‌ای است که حاوی متدها و دیگر اعضای کلاس است و object یک نمونه  (وهله یا instance) از کلاس است.

OOP دارای سه اصل اساسی است: Encapsulation و Inheritance و Polymorphism

با هم مروری خواهیم داشت بر این اصول مهم.

Encapsulation

معمولاً به Encapsulation با قانون "جعبه سیاه" (Black box) اشاره می‌شود. در واقع Encapsulation می‌گوید که وضعیت داخلی یک object باید محافظت شده (protected) بوده و برای موجودیت‌های خارجی (External Entities) غیرقابل دسترس باشد. در واقع کد خارجی نمی‌تواند به صورت مستقیم وضعیت یک شی را ببیند یا تغییر دهد. هر گونه دسترسی به اجزای داخلی از طریق یک Public API انجام می‌شود و این یعنی اطمینان object‌ از اینکه وضعیت داخلی معتبری دارد چرا که وضعیت داخلی اشیا از طریق متدهای عمومی (Public) تغییر می‌کند.

به عنوان یک برنامه‌نویش OOP این مهم هست که وضعیت داخلی (internal state) رو private نگه‌ داریم یا از طریق متدهای public در اختیار کلاس‌های خارجی بگذاریم یا از طریق متدهای protected در اختیار کلاس‌هایی که از کلاس ما ارث‌بری کردند قرار بدهیم.

نتیجه قانون "جعبه سیاه" این است که من به عنوان مصرف‌کننده (consumer) یک object، نباید دلواپس کارهای داخلی آن object باشم. در واقع من فقط باید یاد بگیرم که چطور از API های Public استفاده کنم و می‌توانم مطمئن باشم که هر کاری که می‌خواهم object انجام دهد، توسط منطق اعتبارسنجی داخلی که در object پیاده‌سازی شده مورد بررسی قرار گرفته و در برابر استفاده نادرست محافظت می‌شود. من به این Black box یک ورودی می‌دهم و انتظار یک خروجی دارم و به آنچه در این میان می‌گذرد کاری نخواهم داشت.

اما Encapsulation چه ارتباطی با TDD دارد؟ تست‌ها (آزمون‌های واحد) هم مثل هر کد دیگر Object ها را مصرف (consume) می‌کنند. بنابراین طبق آنچه گفتیم تست باید فقط به Public API کد دسترسی داشته باشد و نه کارهای داخلی کلاس‌ها. این یک مفهوم بسیار مهم برای TDD کاران تازه‌کار است: شما فقط Public API را تست می‌کنید نه متدهای private یا protected.

در ادامه این نوشته‌ها خواهیم دید که متدهای private و protected برای خدمت به Public API ایجاد می‌شوند و بنابراین نیازی به تست کردن آن‌ها نیست و از طریق تست کردن Public API در واقع آن‌ها هم تست می‌شوند.

Inheritance

وقتی در حال توسعه‌دادن نرم‌افزار خود بر پایه کلاس‌ها هستید،‌ متوجه می‌شوید که بعضی کلاس‌ها با هم در ارتباط هستند. بعضی مواقع دو یا چند کلاس شبیه به هم نظر می‌رسند. به عنوان مثال در شکل ز‌یر می‌توانید ببیند که سه کلاس Car‌ و Plane و Boat از چند جنبه به هم شبیه هستند:

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

با Inheritance من می‌توانم یک کلاس پایه (base class) تعریف کنم (در این مثال کلاس Vehicle) و Car و Plane و Boat را از آن ارث‌ ببرم این باعث ایجاد یک رابطه والد/فرزند می‌شود که در شکل زیر نمایش داده می‌شود:

حالا Car‌ و Plane‌ و Boat‌ می‌توانند قابلیت‌های خود شامل سه property به نام‌های FuelCapacityInGallons و PassengerCapacity و RagneOnFullTankInMiles را از کلاس پایه Vehicle‌ ارث‌بری کنند. اگر قابلیت‌های کلاس Vehicle برای هر یک از کلاس‌های مشتق شده (یعنی Car و Plane و Boat) کافی باشد برنامه‌نویس لازم نیست هیچ کار دیگری بکند. با این حال اگر کلاس مشتق شده به یک قابلیت متفاوت نسبت به پیاده‌سازی انجام شده در کلاس پایه نیاز داشته باشد، به راحتی می‌تواند آن قابلیت را با override کردن Property در کلاس خودش، به صورت سفارشی شده پیاده‌سازی کند. شکل زیر این حالت را نشان می‌دهد:

در شکل بالا فراخوانی RangeOnFullTankInMiles در کلاس Plane از نسخه مخصوص پیاده‌سازی شده در خود کلاس Plane استفاده می‌کند در حالی که در کلاس‌های Car و Boat از همان پیاده‌‌سازی انجام شده در کلاس پایه Vehicle استفاده می‌شود.

Polymorphism

اغلب برنامه‌نویس‌ها مفاهیم Encapsulation و Inheritance را خیلی ساده متوجه می‌شوند. اما Polymorphism از آن مفاهیمی است که برنامه‌نویس‌ها معمولاً برای فهمیدنش دچار مشکل هستند و این جای تاسف دارد چرا که بخش زیادی از قدرت اصلی OOP مربوط به توانایی‌های Polymorphism هست.

ایده چندریختی (Polymorphism) این است که گرچه دو کلاس ممکن است آنچنان به هم شبیه باشند که مجموعه رفتار (behaviour) مشابهی را به اشتراک بگذارند و بر اساس این رفتارهای مشابه به شکل یکسان با آن‌ها برخورد شود ولی این امکان وجود دارد که آن رفتارها به شکل‌های متفاوتی پیاده‌سازی شوند

در آخرین شکل بالا، همه وسایل نقلیه از کلاس Vehicle مشتق شده‌اند ولی به شکل‌های مختلفی حرکت می‌کنند: ماشین‌ها در جاده حرکت می‌کنند، کشتی‌ها در آب شناور هستند و هواپیماها در هوا پرواز می‌کنند. ولی همه این‌ها وسلیه نقلیه هستند پس اگر من یک متد داشته باشم که به یک وسلیه نقلیه نیاز داشته و مهم نباشد وسیله نقلیه چه چیزی هست، ‌می‌توانم نوع (Type) از جنس Vehicle را به عنوان پارامتر ورودی در نظر بگیرم و در زمان فراخوانی شی از هر یک از کلاس‌های Car یا Boat یا Plane را به آن پاس بدهم.

در واقع class من حتی لزوماً نیازی نیست بداند که چه نوع وسیله نقلیه‌ای به آن پاس داده شده است چرا که این وسیله نقلیه از کلاس Vehicle مشتق شده که من همه چیزی که در موردش لازم دارم را از طریق property ها و method های عمومی‌اش (Public API) می‌دانم. من می‌توانم تمام property ها و method‌ های عمومی Vehicle را فراخوانی کنم. اگر کلاس وسیله نقلیه‌ای که از آن استفاده می‌کنم، تعریف یا پیاده‌سازی دیگری از یک property یا method داشت می‌توانم از آن تعریف خاص به جای تعریف کلاس پایه Vehicle استفاده کنم.

این یک ایده بسیار قدرتمند است که به کد من قابلیت کلی یا general شدن را می‌دهد. لازم نیست برای هر یک از کلاس‌های مشتق شده از کلاس Vehicle متدهای مختلفی تعریف کنم. کافی است که یک وهله (instance) از کلاس پایه Vehicle را در کدم بپذیرم و این یعنی از هر یک از کلاس‌های مشتق شده از Vehicle نیز می‌توانم استفاده کنم.

اما بر اساس این ایده کاری که نمی‌شود انجام داد این است که متدهایی که به صورت خاص در یک کلاس خاص نوشته شده را استفاده کنیم. مثلاً اگر متدی به نام OpenTrunk در کلاس Car داشته باشم که در کلاس پایه Vehicle تعریف نشده باشد باید قبل از فراخوانی این متد، object وسیله نقلیه را به Car تبدیل (Cast) کنم. به طور کلی اگر دیدید خیلی از این Cast ها در کدتان دارید، احتمالاً یک عیب در طراحی کلاس‌هایتان دارید: یا کلاس پایه شما به اندازه کافی، general نیست یا کلاس پایه، برای متدی که دارید می‌نویسید خیلی خیلی کلی است.

Interface

Polymorphism بر اساس وراثت (Inheritance) همیشه بهترین گزینه نیست. به عنوان مثال کلاس‌های زیر را در نظر بگیرید (propert ها برای بهتر شدن خوانایی دیاگرام برداشته شدند)

به سه کلاس ردیف پایین یعنی Plane و Bird‌ و FlyingSquirrel نگاه کنید. واضح است که همه این کلاس‌ها کار "پرواز کردن" را انجام می‌دهند اما به شیوه‌های متفاوتی. این کلاس‌ها یک رفتار مشترک دارند اما به جز این چیز مشترک دیگری ندارند. دو تا از آن‌ها حیوان هستند و یکی حیوان نیست. تلاش برای قرار دادن این سه کلاس متفاوت در یک کلاس مشترک کار اشتباهی است. پیدا کردن یک abstarction با معنی برای این کلاس‌ها کار بسیار بسیار دشواری است.

خبر خوب این است که لازم نیست برای این کار تلاش کنیم. می‌توانیم از Interface استفاده کنیم. Interface در واقع یک قرارداد است که اعلام می‌کند کد شما Public API مشخصی را پیاده‌سازی و پشتیبانی خواهد کرد. در واقع لیستی از property‌ ها و method‌ های عمومی (public) است که شما قول می‌دهید آن‌ها را در کلاستان تعریف کنید. اینکه این قابلیت چطور تعریف می‌شود جز قرارداد نیست، تنها چیزی که در این قرارداد مهم است این است که آن متدها و ویژگی‌ها وجود خواهند داشت و قابل فراخوانی هستند.

برای اعمال Interface به مساله جاری، من می‌توانم یک Interface به نام IFly با یک متد به نام Fly درست کنم. مرحله بعدی پیاده‌سازی (implement) کردن این Interface در کلاس‌های Plane و Bird‌ و FlyingSquirrel به شکل زیر است:

با توجه به اینکه هر کدام از کلاس‌ها Interface من یعنی IFly را implement کرده‌اند، هر کدام متد Fly خود را خواهند داشت. مثل چندریختی مبتنی بر وراثت (Inheritance based Polymorphism) استفاده از Interface ها این امکان را به من می‌دهد که متدهایی تعریف کنم که پارامتر ورودی از جنس IFly دارند و می‌توانند اشیا از هر نوع کلاسی که این Interface را implement کرده باشد بپذیرند.

شناخت و فهمیدن چندریختی مبتنی بر اینترفیس (interface based Polymorphism) از مهارت‌های ضروری TDD‌ است. وقتی در این سری نوشته‌ها به مبحث mocking‌ و وابستگی‌ها (dependencies) برسیم، دانش شما در این حوزه هر روز به کار خواهد آمد.

ادامه دارد ...

به اشتراک گذاری این نوشته در شبکه‌های اجتماعی

30 روز با TDD: روز اول - TDD چیست و چرا باید از آن استفاده کنم؟

داستان چیه؟

سپتامبر سال گذشته آقای James Bender در وبلا‌گ‌های تلریک یک مجموعه نوشته منتشر کرد به نام 30 روز با TDD. من می‌خوام یک ترجمه آزاد از این نوشته‌ها براتون داشته باشم تا با هم درباره Test Driven Development بیشتر بدونیم. اگر نمی‌دونید داستان TDD چی هست نگران نباشید، این مجموعه نوشته‌ها برای همین هست که با این روش بیشتر آشنا بشیم ;)

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

#30RoozTDD

در توییتر با هشتگ 30RoozTDD# می‌تونید مطالب مرتبط به این سری نوشته‌ها رو پیدا کنید. خود آقای James Bender خوانندگان رو تشویق کرده بود که با هشتگ 30DaysOfTDD# مطلب هر روز رو توییت کنند تا شانس برنده شدن یک لایسنس رایگان نرم‌افزارهای تلریک مثل JustMock یا JustCode رو داشته باشند. من هم از ایده مشابه حمایت می‌کنم، حرکت 30 روز با TDD برای ارتقاء دانش خودم و همه دوستان و همکاران برنامه‌نویسم هست و اگر کسی مایل هست از این حرکت حمایتی بکنه، می‌تونه با من در تماس باشه.

سایر نوشته‌های مربوط به این سری را به زودی در صفحه "30 روز با TDD" می‌تونید ببینید.

و اما اولین روز: TDD چیست و چرا باید ازش استفاده کنم؟

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

TDD چیست؟

قبل از اینکه وارد بحث اینکه TDD‌ دقیقاً‌ چیست و چطوری کار می‌کند بشویم لازم است یک درک مشخص و مشترک از مفهوم Unit Test (آزمون واحد) داشته باشیم. آزمون واحد نوع خیلی خاصی از تست نرم‌افزار، با اهداف و مجموعه ویژگی‌های خیلی روشن است. خوشبختانه تعریف اولیه آزمون واحد، نسبتاً خیلی ساده است: Unit Test یک تست است که یک نیاز مشخص برای یک متد مشخص را آزمایش می‌کند. بعداً در این سری نوشته‌ها به مفاهیم مربوط به row tests و series tests و set tests خواهیم پرداخت ولی در نهایت همه این‌ها unit test هستند و اگر درست پیاده شوند در قانون یک نیاز/ یک متد می‌گنجند. توجه داشته باشید که اگر تست شما،‌ قانون یک نیاز/یک متد را نقض کند، همچنان یک تست است اما دیگر unit test نیست.

آزمون‌های واحد که قانون یک نیاز/یک متد را رعایت می‌کنند، ویژگی‌های زیر را هم به عنوان یک unit test‌ دارند:

  • Targeted: آزمون‌های واحدی که یک چیز (شامل مجموعه‌ای از ورودی‌ها) را در یک زمان آزمایش می‌کنند، هدف‌گیری‌شده هستند.
  • Isolated: کدی که در حال تست آن هستید باید از کد اصلی برنامه و وابستگی‌های خارجی یا رویدادها جدا بوده و ایزوله شده باشد.
  • Repeatable & Predictable: یک آزمون واحد باید قابلیت بارها تکرار مجدد را داشته باشد (Repeatable باشد) و با فرض اینکه کد در حال تست و خود تست تغییر نکنند، هر دفعه همان نتیجه را تولید کند (قابل پیش‌بینی یا Predictable باشد)
  • Independent: این آزمون‌ها باید مستقل باشد، به صورت کلی هیچ تضمینی در خصوص ترتیب اجرا unit test ها وجود ندارد و بنابراین تست‌های نوشته شده توسط شما نباید انتظار یا نیاز به این مساله داشته باشند.

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

Test DRIVEN Development

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

اما در واقعیت، تعهد به نوشتن تست کار بسیار دشواری است و تقریباً همه برنامه‌نویس‌ها بعد از مدتی دچار TED یا Test Eventually Develpoment می‌شوند و در واقع می‌گویند که بالاخره یک روزی تستش می‌کنیم و تا آن یک روزی ممکن است زمان زیادی طول بکشد یا حتی هرگز فرا نرسد!

اولین D در TDD مخفف Driven هست. ایده این روش این است که اولین کاری که برنامه‌نویس انجام می‌دهد نوشتن تست بر اساس ویژگی مورد انتظار فعلی نرم‌افزار (specification) است که روی آن کار می‌کند. این تست‌ها باید fail شوند چرا که قابلیتی که می‌خواهند آزمایش کنند هنوز به وجود نیامده است. در این شرایط کار برنامه‌نویس این خواهد بود که ساده‌ترین کد ممکن را بنویسد یا تست pass شود. اگر نرم‌افزار امکانات مورد انتظار (specification) بیشتری دارد، تست‌های بیشتری بنویسید و چرخه refactor و بهینه کردن کد را ادامه دهید. وقتی همه مشخصات نرم‌افزار تست داشتند و تست‌هایشان pass می‌شد نرم‌افزار شما آماده است. عرضه‌اش کنید!

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

چرا باید از TDD استفاده کنم؟

مزایای زیادی برای استفاده از روش TDD‌ در توسعه نرم‌افزار وجود دارد. برخی از این مزایا واضح هستند و بعضی نه. شاید واضح‌ترین مزیت این باشد که کد شما وقتی کاملاً منطبق بر نیازهای مورد انتظار نرم‌افزار نیست،‌ مشکلات و باگ‌های کمتری خواهد داشت. یکی از انواع باگ‌هایی که TDD می‌تواند به صورت کامل حذفشان کند، "باگ‌های زامبی" هستند: باگ‌هایی که به نظر می‌رسد رفع شده‌اند ولی چند build‌ بعدتر دوباره ظاهر می‌شوند!

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

مزیت دیگر استفاده از TDD بهبود کیفیت کد است.همان‌طور که گفته شد در TDD برنامه‌نویس‌ها باید ساده‌ترین کد برای pass شدن تست‌ها را بنویسند: ساده‌ترین و کوتاه‌ترین کد که معمولاً کیفیت بیشتری دارد. همچنین این کدها خوانایی بیشتری دارند که باعث می‌شود نگهداری کد ساده‌تر شود.

مزیت دیگر استفاده از TDD حذف موثر کدهای مرده از برنامه شماست. کدهای مرده یا Dead Code کدهایی هستند که در برنامه هستند اما هیچ وقت اجرا نمی‌شوند. این کد ممکن است یک متد یا کلاس باشند که هیچ وقت فراخوانی یا ارجاع داده نشدند یا بخشی از یک شرط باشند که هیچ وقت محقق نخواهد شد.
با استفاده از TDD شما فقط کدهایی را می‌نویسید که برای pass شدن تست نیاز دارید. اگر تست‌ها بر اساس نیازمندی‌های نرم‌افزار باشند، هیچ کدی از برنامه نیست که اجرا نشود و کدهایی که با روش TDD ایجاد می‌شوند همیشه مورد استفاده قرار می‌گیرند. با این حال تغییرات در نرم‌افزار به مرور زمان ممکن است باعث شوند یک متد که امروز مورد استفاده قرار می‌گیرد فردا هیچ استفاده‌ای نداشته باشد.

با مانیتور کردن کدها در TDD اگر کدی داشته باشید که در تستی مورد استفاده قرار نگرفته از دو حال خارج نیست: یا یک تست از دست شما در رفته یا آن کد یک کد مرده (dead code) است و باید حذف شود.

ادامه دارد...

به اشتراک گذاری این نوشته در شبکه‌های اجتماعی

دوستان آنلاین و دوستان آفلاین

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

دوستان آنلاین vs دوستان آفلاین

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

چند سال پیش وقتی یکی از دوستان آنلاین برایم دعوتنامه ارکات را فرستاد و بعد من با دقت مشغول تکمیل پروفایل بودم تصور گسترده شدن ایده شبکه‌های اجتماعی دور از ذهن نبود. ارکات به من کمک کرد دوستانی که چندین سال خبری از آن‌ها نداشتم را این بار در اینترنت پیدا کنم. در طی چند سال و تا قبل از انقلاب فیس‌بوک، شبکه‌های اجتماعی رشد قارچ‌گونه‌ای داشتند. یک شبکه اجتماعی تازه می‌آمد و فقط کافی بود یکی دو نفر از دوستانت عضو شده باشند، آن وقت دیگر دعوتنامه‌ها تمامی نداشت. فیس بوک با بازکردن پای برنامه‌نویسان برای تولید برنامه فصل جدیدی در شبکه‌های اجتماعی گشود. از طرف دیگر، حالا سایت‌هایی مثل ning این امکان را به کاربران می‌دهند تا شبکه اجتماعی خود را داشته باشند، یا مثلا Google Friend Connect به وبلاگ‌نویسان کمک می‌کنند تا شبکه‌ای از دوستان و خوانندگان داشته باشند. (حالا بعد از گذشت چند سال، نفوذ شبکه‌های اجتماعی در زندگی ما ده‌ها برابر شده است)

چند دوست؟

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

در چنین شرایطی است که مشکلات اعتیاد به وب2 خودشان را نشان می‌دهند:

  • آیا بیشتر از زمانی که با دوستان واقعی‌تان زمان صرف می‌کنید، با دوستان آنلاین خودتان در ارتباط هستید؟ همان‌طور که گفتم دسترسی به دوستان جدید در محیط‌های آنلاین بسیار ساده‌تر و کم هزینه‌تر است. سرویس‌هایی هستند که آمار استفاده از سایت‌ها و برنامه‌ها را در اختیار می‌گذارند. سرویس قدیمی واکوپا یکی از آن‌هاست. در آمار واکوپا شخصی را دیدم که به طور متوسط در یک ماه گذشته روزی نزدیک به 5 ساعت از فرندفید استفاده کرده. به نظر شما ممکن است روزی فرندفید، جای گپ‌های دوستانه و چهره به چهره ما را بگیرد؟ (این نوشته مربوط به چند سال پیش و اوج زمان استفاده از فرندفید بود، حالا فرندفید را با فیس‌بوک و توییتر عوض کنید)
  • مشکلات ارتباطی، خب من دیگه دوست ندارم با تو ارتباطی داشته باشم: در دنیای واقعی، به همین سادگی نیست که دکمه block را فشار بدهید و دیگه اثری از آن شخص در زندگی شما نباشد. فناوری‌های نو روش ارتباط ما خیلی تغییر داده‌اند. تا به حال شده است وقتی یک کتاب چاپی یا مطلب جالب را در روزنامه می‌خوانید پایین صفحه به سبک (مرحوم) گوگل ریدر دنبال دکمه share باشید؟ البته این مساله فقط درباره وب نیست. از چند سال پیش به این طرف فکر نمی‌کنید sms جای مکالمه تلفنی با دوستانتان را گرفته؟
  • سرسری خواندن: وقتی دوستان آنلاین زیادی داشته باشید با حجم زیادی از محتوا مواجه می‌شوید که دوستان شما تولید کرده‌اند یا به اشتراک گذاشته‌اند و شما از ترس از دست دادن حتی یک مورد جالب/مفید/سرگرم کننده دوست دارید همه‌شان را مرور کنید. آیا شما هم مثل من این وسواس را پیدا کرده‌اید که آیتم‌های خوانده نشده (مرحوم) گوگل ریدرتان همیشه صفر باشد؟ وقتی بخواهید تعداد زیادی مطلب را بخوانید و در مورد share کردن یا reply کردن یا comment گذاشتن یا افزودن به favorites یا هر اکشن دیگری تصمیم بگیرید، یواش یواش می‌بینید که به سرسری خواندن عادت کرده‌اید و اصلاً حوصله خواندن متن‌های طولانی را ندارید. دیگر نمی‌توانید یک کتاب قطور را با حوصله بخوانید و به دنبال این هستید که با خواندن تصادفی بخش‌هایی از این پست من، هر چه زودتر به پیام آن برسید!این یک خطر بزرگ است. توصیه می‌کنم حتماً وقت بگذارید و چطور اینترنت ما را به سطحی خواندن عادت داد و برای درمانش چه کنیم؟ را مطالعه کنید.

شما چه تجربه یا ایده‌ای در این مورد دارید؟ 

به اشتراک گذاری این نوشته در شبکه‌های اجتماعی

پنج دلیل اصلی (اشتباه) که شما تست کننده نرم‌افزار ندارید

در نوشته مربوط به تست دهم جوئل، از تست نرم‌افزارها گفتم و همانجا اشاره کردم که مطلب 5 دلیل اصلی اشتباه استخدام نکرد Tester ها از جوئل اسپالسکی را هم ترجمه می‌کنم. امروز در ترجمه‌ای آزاد دلایلی که آقای اسپالسکی برای نبودن تست کننده نرم‌افزار مطرح کرده را با هم بررسی می‌کنیم:

دلیل اشتباه اول: باگ‌ها توسط برنامه‌نویسان تنبل ایجاد می‌شوند

دید اشتباه این هست که "اگر تست‌کننده نرم‌افزار استخدام کنیم" آن‌‌گاه "برنامه‌نویس‌ها، تنبل می‌شوند و کدهای باگ‌دار تولید می‌کنند. با استخدام نکردن تست‌کننده نرم‌افزار، برنامه‌نویسان مجبور می‌شوند که از همان اول کد خوب و بی‌باگ ایجاد کنند"

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

دلیل اشتباه دوم. برنامه من تحت وب است و هر باگی را در چند ثانیه می‌توانم حل کنم

این اشاره درستی هست که کار رفع باگ‌ها (bug fix) در نرم‌افزارهای تحت وب سریع‌تر از نرم‌افزارهای دسکتاپی است اما باید به نکات جانبی رفع باگ نیز توجه داشت. چه اتفاقی می‌افتد اگر رفع باگی که انجام دادید باعث ایجاد چند باگ دیگر شود؟ اگر به دقت به روال رفع باگ‌ در برنامه‌های تحت وب نگاه کنید، شاید به این نتیجه برسید که این کار خیلی پرهزینه‌تر از آن است که در ابتدا تصور می‌شود و این تازه صرفنظر از تاثیر بدی است که بر دید مشتریان می‌تواند داشته باشد.

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


عواقب استفاده از محصولی که به درستی تست نشده ممکن است جبران ناپذیر باشد: صحنه معروف کلاه‌ها در فیلم پرستیژ

دلیل اشتباه سوم: مشتریان، نرم‌افزار را برای من تست خواهند کرد

جوئل در توضیح اشتباه بودن این دلیل، متدولوژی مسخره netscape برای تست نرم‌افزارهایش (که به قول جوئل به شکل ماورا‌ الطبیعی به اعتبار شرکت صدمه وارد کرد) را توضیح می‌دهد:

  • وقتی برنامه‌نویسان، هنوز نیمی از کار را انجام دادند، نرم‌افزار را بدون تست در وب منتشر کن
  • وقتی برنامه‌نویسان می‌گویند که کار تمام شده، نرم‌افزار را بدون تست در وب منتشر کن
  • این کار را شش هفت بار تکرار کن
  • یکی از نسخه‌هایی که با این روش منتشر کردی را "نسخه نهایی" نام‌گذاری کن
  • هر دفعه که به یک باگ که باعث شرمساری می‌شود در سایت cnet اشاره شد،‌ نسخه‌های 0.1,0.2,0.3 را منتشر کن!
نتیجه این روش این شد که مشتریان بسیار زیادی در دنیا، همیشه و همیشه با نرم‌افزارهای باگ‌دار netscape کار کردند و این باعث شد حتی وقتی که نسخه نسبتاً بی‌باگی از نرم‌افزار release می‌شد، باز هم دید عمومی به آن به شکل یک نرم‌افزار باگ‌دار باشد.
 
مشکل بدتر در این مثال این بود که کل ایده اینکه باگ‌ها توسط مشتریان کشف و اعلام شوند، برای این بود که بعد از اعلام مشتری، شرکت بتواند باگ‌ها را رفع کند. اما متاسفانه نه netscape و نه هیچ شرکتی در دنیا، امکانات و نیروی انسانی و توان فنی برای بررسی اولویت‌ها و رفع باگ‌های اعلامی بیش از 2 میلیون مشتری را نداشت. در نتیجه باگ‌ها و ایرادات مهم در میان انبوه درخواست‌‌های رسیده دفن می‌شدند.
 
احتمالاً می‌توانید تصور کنید که چه صدمه‌ای به اعتبار شرکت زده شد و امروز هم netscape تبدیل شده به مثالی برای درس‌ها و عبرت‌های این چنینی.
 
دلیل اشتباه چهارم: کسانی که شایستگی لازم برای تست کردن نرم‌افزار را دارند نمی‌خواهند تست کننده نرم‌افزار باشند
 
این یک واقعیت هست که پیدا کردن تست‌کننده خوب نرم‌افزار مثل برنامه‌نویس خوب مشکل است. جوئل اشاره می‌کند که در شرکت Juno که کار می‌کرده،‌ یکی از تست‌کننده‌های نرم‌افزار به تنهایی سه برابر مجموع همه باگ‌هایی که 4 تست کننده دیگر نرم‌افزار گزارش کرده بودند را کشف و گزارش کرده.
جوئل اشاره می‌کند که اغلب تست‌کننده‌های نرم‌افزار خوب بعد از مدتی حوصله‌شان از کار روتین تست نرم‌افزار به صورت روزانه سر می‌رود. این مشکلی هست که وجود دارد و باید آن را پذیرفت. پیشنهادهای جوئل برای این مساله این‌ها هستند:
  •  تست نرم‌افزار را به عنوان یک ارتقاء شغلی از بخش پشتیبانی مشتریان در نظر بگیرید
  • به تست‌کننده‌های خوب و باهوش اجازه بدهید که تا حدی برنامه‌نویسی کار کنند و unit test با استفاده از ابزارهای برنامه‌نویسی ایجاد کنند. این کار خیلی جذاب‌تر از تست دوباره و دوباره و دوباره و دوباره یک پنجره دیالوگ در نرم‌افزار هست.
  • بپذیرید که بعد از مدتی تست‌کننده‌های خوب شما را ترک خواهند کرد. همیشه در پی استخدام تست‌کننده باشید و روند استخدام آن‌ها را به خاطر اینکه موقتاً ظرفیت شما تکمیل شده را متوقف نکنید.
  • به صورت موقت استخدام کنید. اگر به عنوان مثال 10 نفر را به صورت موقت و چند روزه برای تست نرم‌افزاری استخدام کنید، خواهید دید که ده‌ها باگ پیدا می‌شود. بعضی از این افراد موقت، ممکن است توانمندی‌های لازم برای تبدیل شدن به یک تست‌کننده نرم‌افزار را داشته باشند که در این صورت می‌توانید آن‌ها را به صورت تمام وقت استخدام کنید.
دلیل اشتباه پنجم: من توان مالی برای استخدام تست‌کننده نرم‌افزار را ندارم!
 
این احمقانه‌ترین دلیل برای استخدام نکردن تست‌کننده نرم‌افزار است. مهم نیست که چقدر پیدا کردن تست‌کننده نرم‌افزار سخت باشد، به هر حال آن‌ها ارزان‌تر از برنامه‌نویس‌ها هستند. خیلی ارزان‌تر.
اگر شما از تست‌کننده نرم‌افزار استفاده نکنید، برنامه‌نویس شما باید کار تست را انجام دهد و علاوه بر هزینه‌ای که این کار به شما تحمیل می‌کند، باید صبر کنید و منتظر لحظه‌ای باشید که برنامه‌نویس ارشد شما از اینکه بارها به وی گفته شده که "چند هفته‌ بیشتر روی تست نرم‌افزار وقت بگذار قبل از اینکه منتشرش کنی" خسته می‌شود و می‌رود تا در شرکتی حرفه‌ای‌تر کار کند.
شما می‌توانید 3 تست‌کننده نرم‌افزار را برای یک سال استخدام کنید و هنوز ارزان‌تر از هزینه جایگزینی یک برنامه‌نویس ارشد هست.

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

به اشتراک گذاری این نوشته در شبکه‌های اجتماعی