‏نمایش پست‌ها با برچسب Unit testing. نمایش همه پست‌ها
‏نمایش پست‌ها با برچسب Unit testing. نمایش همه پست‌ها

۱۳۹۰/۱۰/۲۰

قبل از رفع باگ، براي آن تست بنويسيد


از دقت كردن در نحوه اداره پروژه‌هاي خوب و بزرگ در سطح دنيا، مي‌توان به نكات آموزنده‌اي رسيد. براي مثال NHibernate را درنظر بگيريد. اين پروژه شايد روز اول كپي مطابق اصل نمونه جاواي آن بوده، اما الان از خيلي از جهات يك سر و گردن از آن بالاتر است. پشتيباني LINQ را اضافه كرده، خودش Syntax جديدي را به نام QueryOver ارائه داده و همچنين معادلي را جهت حذف فايل‌هاي XML به كمك امكانات جديد زبان‌هاي دات نتي مانند lambda expressions ارائه كرده. خلاصه اين تيم، فقط يك كپي كار نيست. پايه رو از يك جايي گرفته اما سبب تحول در آن شده. از اهداف پروژه‌هاي سورس باز هم همين است: براي هر كاري چرخ را از صفر ابداع نكنيد.

اگر به نحوه اداره كلي پروژه NHibernate‌ دقت كنيد يك مورد مشهود است:
  • تمام گزارش‌هاي باگ بدون Unit test نديد گرفته مي‌شوند.
  • از كليه بهبودهاي ارائه شده (وصله‌ها يا patch ها) بدون Unit test صرفنظر مي‌شود.
  • از كليه موارد جديد ارائه شده بدون Unit test هم صرفنظر خواهد شد.

بنابراين اگر در issue tracker اين تيم رفتيد و گفتيد: «سلام، اينجا اين مشكل هست»، خيالتان راحت باشد كه نديد گرفته خواهيد شد.

سؤال : چرا اين‌ها اينطور رفتار مي‌كنند؟!
- وجود Unit test دقيقا مشخص مي‌كند كه چه قسمت يا قسمت‌هايي به گزارش باگ شما مرتبط هستند. نيازي نيست حتما بتوانيد يك خطا را با جملات ساده شرح دهيد. اين مساله خصوصا در پروژه‌هاي بين المللي حائز اهميت است. ضعف زبان انگليسي همه جا هست. همينقدر كه توانسته‌ايد براي آن يك Unit test بنويسيد كه مثلا در اين عمليات با اين ورودي،‌ نتيجه قرار بوده بشود 10 و مثلا شده 5 يا حتي اين Exception صادر شده كه بايد كنترل شود، يعني مشكل را كاملا مشخص كرده‌ايد.
- وجود Unit tests ، انجام Code review و همچنين Refactoring را تسهيل مي‌بخشند. در هر دو حالت ياد شده، هدف تغيير كاركرد سيستم نيست؛ هدف بهبود كيفيت كدهاي موجود است. بنابراين دست به يك سري تغييرات زده خواهد شد. اما سؤال اينجا است كه از كجا بايد مطمئن شد كه اين تغييرات، سيستم را به هم نريخته‌اند. پروژه‌ي جاري چند سال است كه در حال توسعه است. قسمت‌هاي زيادي به آن اضافه شده. با نبود Unit tests ممكن است بعضي از قسمت‌ها زايد يا احمقانه به نظر برسند.
- بهترين مستندات كدهاي تهيه شده، Unit tests آن هستند. براي مثال علاقمند هستيد كه NHibernate را ياد بگيريد؟ هرچه مي‌گرديد مثال‌هاي كمي را در اينترنت در اين زمينه پيدا مي‌كنيد؟ وقت خودتان را تلف نكنيد! اين پروژه بالاي 2000 آزمون واحد دارد. هر كدام از اين آزمون‌ها نحوه‌ي بكارگيري قسمت‌هاي مختلف را به نحوي كاربردي بيان مي‌كنند.
- وجود Unit tests از پيدايش مجدد باگ‌ها جلوگيري مي‌كنند. اگر آزمون واحدي وجود نداشته باشد، امروز كدي اضافه مي‌شود. فردا همين كد توسط عده‌اي ديگر زايد تشخيص داده شده و حذف مي‌شود! بنابراين احتمال بروز مجدد اين خطا در آينده وجود خواهد داشت. با وجود Unit tests، فلسفه وجودي هر قسمتي از كدهاي موجود پروژه دقيقا مشخص مي‌شود و در صورت حذف آن‌، با اجراي آزمون‌هاي خودكار سريعا مي‌توان به كمبودهاي حاصل پي‌برد.

۱۳۹۰/۰۶/۲۸

تبديل عدد به حروف


به طور قطع توابع و كلاس‌هاي تبديل عدد به حروف، در جعبه ابزار توابع كمكي شما هم پيدا مي‌شوند. روز قبل سعي كردم جهت آزمايش، عدد 3000,000,000,000,000 ريال را با كلاسي كه دارم تست كنم و نتيجه overflow يا اصطلاحا تركيدن سيستم بود! البته اگر مطالب اين سايت را دنبال كرده باشيد پيشتر در همين راستا مطلبي در مورد نحوه‌ي صحيح بكارگيري توابع تجمعي SQL در اين سايت منتشر شده است و جزو الزامات هر سيستمي است (تفاوتي هم نمي‌كند كه به چه زباني تهيه شده باشد). اگر آ‌ن‌را رعايت نكرده‌ايد، سيستم شما «روزي» دچار overflow خواهد شد.

در كل اين كلاس تبديل عدد به حروف را به صورت ذيل اصلاح كردم و همچنين دو زبانه است؛ چيزي كه كمتر در پياده سازي‌هاي عمومي به آن توجه شده است:

using System.Collections.Generic;
using System.Linq;

namespace NumberToWordsLib
{
 /// <summary>
 /// Number to word languages
 /// </summary>
 public enum Language
 {
  /// <summary>
  /// English Language
  /// </summary>
  English,

  /// <summary>
  /// Persian Language
  /// </summary>
  Persian
 }

 /// <summary>
 /// Digit's groups
 /// </summary>
 public enum DigitGroup
 {
  /// <summary>
  /// Ones group
  /// </summary>
  Ones,

  /// <summary>
  /// Teens group
  /// </summary>
  Teens,

  /// <summary>
  /// Tens group
  /// </summary>
  Tens,

  /// <summary>
  /// Hundreds group
  /// </summary>
  Hundreds,

  /// <summary>
  /// Thousands group
  /// </summary>
  Thousands
 }

 /// <summary>
 /// Equivalent names of a group 
 /// </summary>
 public class NumberWord
 {
  /// <summary>
  /// Digit's group
  /// </summary>
  public DigitGroup Group { set; get; }

  /// <summary>
  /// Number to word language
  /// </summary>
  public Language Language { set; get; }

  /// <summary>
  /// Equivalent names
  /// </summary>
  public IList<string> Names { set; get; }
 }

 /// <summary>
 /// Convert a number into words
 /// </summary>
 public static class HumanReadableInteger
 {
  #region Fields (4)

  private static readonly IDictionary<Language, string> And = new Dictionary<Language, string>
  {
   { Language.English, " " },
   { Language.Persian, " و " } 
  };
  private static readonly IList<NumberWord> NumberWords = new List<NumberWord>
  {
   new NumberWord { Group= DigitGroup.Ones, Language= Language.English, Names=
    new List<string> { string.Empty, "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine" }},
   new NumberWord { Group= DigitGroup.Ones, Language= Language.Persian, Names=
    new List<string> { string.Empty, "يك", "دو", "سه", "چهار", "پنج", "شش", "هفت", "هشت", "نه" }},

   new NumberWord { Group= DigitGroup.Teens, Language= Language.English, Names=
    new List<string> { "Ten", "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen" }},
   new NumberWord { Group= DigitGroup.Teens, Language= Language.Persian, Names=
    new List<string> { "ده", "يازده", "دوازده", "سيزده", "چهارده", "پانزده", "شانزده", "هفده", "هجده", "نوزده" }},

   new NumberWord { Group= DigitGroup.Tens, Language= Language.English, Names=
    new List<string> { "Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety" }},
   new NumberWord { Group= DigitGroup.Tens, Language= Language.Persian, Names=
    new List<string> { "بيست", "سي", "چهل", "پنجاه", "شصت", "هفتاد", "هشتاد", "نود" }},

   new NumberWord { Group= DigitGroup.Hundreds, Language= Language.English, Names=
    new List<string> {string.Empty, "One Hundred", "Two Hundred", "Three Hundred", "Four Hundred", 
     "Five Hundred", "Six Hundred", "Seven Hundred", "Eight Hundred", "Nine Hundred" }},
   new NumberWord { Group= DigitGroup.Hundreds, Language= Language.Persian, Names=
    new List<string> {string.Empty, "يكصد", "دويست", "سيصد", "چهارصد", "پانصد", "ششصد", "هفتصد", "هشتصد" , "نهصد" }},

   new NumberWord { Group= DigitGroup.Thousands, Language= Language.English, Names=
     new List<string> { string.Empty, " Thousand", " Million", " Billion"," Trillion", " Quadrillion", " Quintillion", " Sextillian",
   " Septillion", " Octillion", " Nonillion", " Decillion", " Undecillion", " Duodecillion", " Tredecillion",
   " Quattuordecillion", " Quindecillion", " Sexdecillion", " Septendecillion", " Octodecillion", " Novemdecillion",
   " Vigintillion", " Unvigintillion", " Duovigintillion", " 10^72", " 10^75", " 10^78", " 10^81", " 10^84", " 10^87",
   " Vigintinonillion", " 10^93", " 10^96", " Duotrigintillion", " Trestrigintillion" }},
   new NumberWord { Group= DigitGroup.Thousands, Language= Language.Persian, Names=
     new List<string> { string.Empty, " هزار", " ميليون", " ميليارد"," تريليون", " Quadrillion", " Quintillion", " Sextillian",
   " Septillion", " Octillion", " Nonillion", " Decillion", " Undecillion", " Duodecillion", " Tredecillion",
   " Quattuordecillion", " Quindecillion", " Sexdecillion", " Septendecillion", " Octodecillion", " Novemdecillion",
   " Vigintillion", " Unvigintillion", " Duovigintillion", " 10^72", " 10^75", " 10^78", " 10^81", " 10^84", " 10^87",
   " Vigintinonillion", " 10^93", " 10^96", " Duotrigintillion", " Trestrigintillion" }},
  };
  private static readonly IDictionary<Language, string> Negative = new Dictionary<Language, string>
  {
   { Language.English, "Negative " },
   { Language.Persian, "منهاي " } 
  };
  private static readonly IDictionary<Language, string> Zero = new Dictionary<Language, string>
  {
   { Language.English, "Zero" },
   { Language.Persian, "صفر" } 
  };

  #endregion Fields

  #region Methods (7)

  // Public Methods (5) 

  /// <summary>
  /// display a numeric value using the equivalent text
  /// </summary>
  /// <param name="number">input number</param>
  /// <param name="language">local language</param>
  /// <returns>the equivalent text</returns>
  public static string NumberToText(this int number, Language language)
  {
   return NumberToText((long)number, language);
  }


  /// <summary>
  /// display a numeric value using the equivalent text
  /// </summary>
  /// <param name="number">input number</param>
  /// <param name="language">local language</param>
  /// <returns>the equivalent text</returns>
  public static string NumberToText(this uint number, Language language)
  {
   return NumberToText((long)number, language);
  }

  /// <summary>
  /// display a numeric value using the equivalent text
  /// </summary>
  /// <param name="number">input number</param>
  /// <param name="language">local language</param>
  /// <returns>the equivalent text</returns>
  public static string NumberToText(this byte number, Language language)
  {
   return NumberToText((long)number, language);
  }

  /// <summary>
  /// display a numeric value using the equivalent text
  /// </summary>
  /// <param name="number">input number</param>
  /// <param name="language">local language</param>
  /// <returns>the equivalent text</returns>
  public static string NumberToText(this decimal number, Language language)
  {
   return NumberToText((long)number, language);
  }

  /// <summary>
  /// display a numeric value using the equivalent text
  /// </summary>
  /// <param name="number">input number</param>
  /// <param name="language">local language</param>
  /// <returns>the equivalent text</returns>
  public static string NumberToText(this double number, Language language)
  {
   return NumberToText((long)number, language);
  }

  /// <summary>
  /// display a numeric value using the equivalent text
  /// </summary>
  /// <param name="number">input number</param>
  /// <param name="language">local language</param>
  /// <returns>the equivalent text</returns>
  public static string NumberToText(this long number, Language language)
  {
   if (number == 0)
   {
    return Zero[language];
   }

   if (number < 0)
   {
    return Negative[language] + NumberToText(-number, language);
   }

   return wordify(number, language, string.Empty, 0);
  }
  // Private Methods (2) 

  private static string getName(int idx, Language language, DigitGroup group)
  {
   return NumberWords.Where(x => x.Group == group && x.Language == language).First().Names[idx];
  }

  private static string wordify(long number, Language language, string leftDigitsText, int thousands)
  {
   if (number == 0)
   {
    return leftDigitsText;
   }

   var wordValue = leftDigitsText;
   if (wordValue.Length > 0)
   {
    wordValue += And[language];
   }

   if (number < 10)
   {
    wordValue += getName((int)number, language, DigitGroup.Ones);
   }
   else if (number < 20)
   {
    wordValue += getName((int)(number - 10), language, DigitGroup.Teens);
   }
   else if (number < 100)
   {
    wordValue += wordify(number % 10, language, getName((int)(number / 10 - 2), language, DigitGroup.Tens), 0);
   }
   else if (number < 1000)
   {
    wordValue += wordify(number % 100, language, getName((int)(number / 100), language, DigitGroup.Hundreds), 0);
   }
   else
   {
    wordValue += wordify(number % 1000, language, wordify(number / 1000, language, string.Empty, thousands + 1), 0);
   }

   if (number % 1000 == 0) return wordValue;
   return wordValue + getName(thousands, language, DigitGroup.Thousands);
  }

  #endregion Methods
 }
}



دريافت پروژه كامل به همراه Unit tests مرتبط


۱۳۹۰/۰۶/۱۷

استفاده يكپارچه از NUnit در VS.NET بدون نياز به افزونه‌ها


براي استفاده ساده‌تر از ابزارهاي unit testing در ويژوال استوديو افزونه‌هاي زيادي وجود دارند، از ري شارپر تا CodeRush  تا حتي امكانات نسخه‌ي كامل VS.NET كه با MSTest يكپارچه است. اما اگر نخواهيم از MSTest استفاده كنيم و همچنين افزونه‌ها را هم بخواهيم حذف كنيم (مثلا از نسخه‌ي رايگان express استفاده كنيم)، چطور؟
براي حل اين مشكل چندين روش وجود دارد. يا مي‌شود از test runner اين‌ها استفاده كرد كه اصلا نيازي به IDE ندارند و مستقل است؛ يا مي‌توان به صورت زير هم عمل كرد:
به خواص پروژه در VS.NET مراجعه كنيد. برگه‌ي Build events را باز كنيد. در اينجا مي‌خواهيم post-build event را مقدار دهي كنيم. به اين معنا كه پس از هر build موفق، لطفا اين دستورات خط فرمان را اجرا كن.
NUnit به همراه test runner خط فرمان هم ارائه مي‌شود و نام آن nunit-console.exe است. اگر به محل نصب آن مراجعه كنيد، عموما در آدرس C:\Program Files\NUnit xyz\bin\nunit-console.exe قرار دارد. براي استفاده از آن تنها كافي است تنظيم زير صورت گيرد:

c:\path\nunit-console.exe /nologo $(TargetPath)

TargetPath به صورت خودكار با نام اسمبلي جاري پروژه در زمان اجرا جايگزين مي‌شود.
اكنون پس از هر Build، به صورت خودكار nunit-console.exe اجرا شده، اسمبلي برنامه كه حاوي آزمون‌هاي واحد است به آن ارسال گرديده و سپس خروجي كار در output window نمايش داده مي‌شود. اگر خطايي هم رخ داده باشد در قسمت errors قابل مشاهده خواهد بود.
در اينجا حتي بجاي برنامه كنسول ياده شده مي‌توان از برنامه nunit.exe هم استفاده كرد. در اين حالت GUI اصلي پس از هر Build نمايش داده مي‌شود:

c:\path\nunit.exe $(TargetPath)


چند نكته:
1- برنامه nunit-console.exe چون در حال حاضر براي دات نت 2 كامپايل شده امكان بارگذاري dll هاي دات نت 4 را ندارد. به همين منظور فايل nunit-console.exe.config را باز كرده و تنظيمات زير را به آن اعمال كنيد:

<configuration>  
<startup>  
      <supportedRuntime version="v4.0.30319" />  
</startup>  

و همچنين:

<runtime>  
    <loadFromRemoteSources enabled="true" />  

2- خروجي نتايج اجراي آزمون‌ها را به صورت XML هم مي‌توان ذخيره كرد. مثلا:

c:\path\nunit-console.exe /xml:$(ProjectName)-tests.xml /nologo $(TargetPath)



3- از فايل xml ذكر شده مي‌توان گزارشات زيبايي تهيه كرد. براي مثال:
Generating Report for NUnit
NUnit2Report Task


جهت مطالعه بيشتر:
Setting up Visual C#2010 Express with NUnit
Use Visual Studio's Post-Build Events to Automate Unit Testing Running
3 Ways to Run NUnit From Visual Studio


۱۳۹۰/۰۲/۰۵

تهيه آزمون واحد جهت كار با محتواي فايل‌ها


يكي از شروط تهيه‌ آزمون‌هاي واحد، خارج نشدن از مرزهاي سيستم در حين بررسي آزمون‌هاي مورد نظر است؛ تا بتوان تمام آزمون‌ها را با سرعت بسيار بالايي، بدون نگراني از در دسترس نبودن منابع خارجي، درست در لحظه انجام آزمون‌ها، به پايان رساند. اگر اين خروج صورت گيرد، بجاي unit tests با integration tests سر و كار خواهيم داشت. در اين ميان، كار با فايل‌ها نيز مصداق بارز خروج از مرزهاي سيستم است.
براي حل اين مشكل راه حل‌هاي زيادي توصيه شده‌اند؛ منجمله تهيه يك اينترفيس محصور كننده فضاي نام System.IO و سپس استفاده از فريم ورك‌هاي mocking و امثال آن. يك نمونه از پياده سازي آن‌را اينجا مي‌توانيد پيدا كنيد : (+)
اما راه حل ساده‌تري نيز براي اين مساله وجود دارد و آن هم افزودن فايل‌هاي مورد نظر به پروژه آزمون واحد جاري و سپس مراجعه به خواص فايل‌ها و تغيير Build Action آن‌‌ها به Embedded Resource مي‌باشد. به اين صورت پس از كامپايل پروژه، فايلهاي ما در قسمت منابع اسمبلي جاري قرار گرفته و به كمك متد زير قابل دسترسي خواهند بود:
using System.IO;
using System.Reflection;

public class UtHelper
{
public static string GetInputFile(string filename)
{
var thisAssembly = Assembly.GetExecutingAssembly();
var stream = thisAssembly.GetManifestResourceStream(filename);
return new StreamReader(stream).ReadToEnd();
}
}

نكته‌اي را كه اينجا بايد به آن دقت داشت، filename متد GetInputFile است. چون اين فايل ديگر به صورت متداول از فايل سيستم خوانده نخواهد شد، نام واقعي آن به صورت namespace.filename مي‌باشد (همان نام منبع اسمبلي جاري).
اگر جهت يافتن اين نام با مشكل مواجه شديد، تنها كافي است اسمبلي آزمون واحد را با برنامه Reflector يا ابزارهاي مشابه گشوده و نام منابع آن‌را بررسي كنيد.

۱۳۹۰/۰۱/۲۴

بررسي ميزان پوشش آزمون‌هاي واحد به كمك برنامه PartCover


هميشه در حين توسعه‌ي يك برنامه اين سؤالات وجود دارند:
- چند درصد از برنامه تست شده است؟
- براي چه تعدادي از متدهاي موجود آزمون واحد نوشته‌ايم؟
- آيا همين آزمون‌هاي واحد نوشته شده و موجود، كامل هستند و تمام عملكرد‌هاي متدهاي مرتبط را پوشش مي‌دهند؟

اين سؤالات به صورت خلاصه مفهوم Code coverage را در بحث Unit testing ارائه مي‌دهند: براي چه قسمت‌هايي از برنامه آزمون واحد ننوشته‌ايم و ميزان پوشش برنامه توسط آزمون‌هاي واحد موجود تا چه حدي است؟
بررسي اين سؤالات در يك پروژه‌ي كم حجم، ساده بوده و به صورت بازبيني بصري ممكن است. اما در يك پروژه‌ي بزرگ نياز به ابزار دارد. به همين منظور تعدادي برنامه جهت بررسي code coverage مختص پروژه‌هاي دات نتي تابحال توليد شده‌اند كه در ادامه ليست آن‌ها را مشاهده مي‌كنيد:
و ...

تمام اين‌ها تجاري هستند. اما در اين بين برنامه‌ي PartCover سورس باز و رايگان بوده و همچنين مختص به NUnit نيز تهيه شده است. اين برنامه را از اينجا مي‌توانيد دريافت و نصب كنيد. در ادامه نحوه‌ي تنظيم آن‌را بررسي خواهيم كرد:

الف) ايجاد يك پروژه آزمون واحد جديد
جهت توضيح بهتر سه سؤال مطرح شده در ابتداي اين مطلب، بهتر است يك مثال ساده را در اين زمينه مرور نمائيم: (پيشنياز: (+))
يك Solution جديد در VS.NET آغاز شده و سپس دو پروژه جديد از نوع‌هاي كنسول و Class library به آن اضافه شده‌اند:



پروژه كنسول، برنامه اصلي است و در پروژه Class library ، آزمون‌هاي واحد برنامه را خواهيم نوشت.
كلاس اصلي برنامه كنسول به شرح زير است:
namespace TestPartCover
{
public class Foo
{
public int DoFoo(int x, int y)
{
int z = 0;
if ((x > 0) && (y > 0))
{
z = x;
}
return z;
}

public int DoSum(int x)
{
return ++x;
}
}
}
و كلاس آزمون واحد آن در پروژه class library مثلا به صورت زير خواهد بود:
using NUnit.Framework;

namespace TestPartCover.Tests
{
[TestFixture]
public class Tests
{
[Test]
public void TestDoFoo()
{
var result = new Foo().DoFoo(-1, 2);
Assert.That(result == 0);
}
}
}
كه نتيجه‌ي بررسي آن توسط NUnit test runner به شكل زير خواهد بود:



به نظر همه چيز خوب است! اما آيا واقعا اين آزمون كافي است؟!

ب) در ادامه به كمك برنامه‌ي PartCover مي‌خواهيم بررسي كنيم ميزان پوشش آزمون‌هاي واحد نوشته شده تا چه حدي است؟

پس از نصب برنامه، فايل PartCover.Browser.exe را اجرا كرده و سپس از منوي فايل، گزينه‌ي Run Target را انتخاب كنيد تا صفحه‌ي زير ظاهر شود:



توضيحات:
در قسمت executable file آدرس فايل nunit-console.exe را وارد كنيد. اين برنامه چون در حال حاضر براي دات نت 2 كامپايل شده امكان بارگذاري dll هاي دات نت 4 را ندارد. به همين منظور فايل nunit-console.exe.config را باز كرده و تنظيمات زير را به آن اعمال كنيد (مهم!):
<configuration>
<startup>
<supportedRuntime version="v4.0.30319" />
</startup>

و همچنين
<runtime>
<loadFromRemoteSources enabled="true" />

در ادامه مقابل working directory‌ ، آدرس پوشه bin پروژه unit test را تنظيم كنيد.
در اين حالت working arguments به صورت زير خواهند بود (در غيراينصورت بايد مسير كامل را وارد نمائيد):
TestPartCover.Tests.dll /framework=4.0.30319 /noshadow

نام dll‌ وارد شده همان فايل class library توليدي است. آرگومان بعدي مشخص مي‌كند كه قصد داريم يك پروژه‌ي دات نت 4 را توسط NUnit بررسي كنيم (اگر ذكر نشود پيش فرض آن دات نت 2 خواهد بود و نمي‌تواند اسمبلي‌هاي دات نت 4 را بارگذاري كند). منظور از noshadow اين است كه NUnit‌ مجاز به توليد shadow copies از اسمبلي‌هاي مورد آزمايش نيست. به اين صورت برنامه‌ي PartCover مي‌تواند بر اساس StackTrace نهايي، سورس متناظر با قسمت‌هاي مختلف را نمايش دهد.
اكنون نوبت به تنظيم Rules آن است كه يك سري RegEx هستند؛ به عبارتي چه اسمبلي‌هايي آزمايش شوند و كدام‌ها خير:
+[TestPartCover]*
-[nunit*]*
-[log4net*]*

همانطور كه ملاحظه مي‌كنيد در اينجا از اسمبلي‌هاي NUnit و log4net صرفنظر شده است و تنها اسمبلي TestPartCover (همان برنامه كنسول، نه اسمبلي برنامه آزمون واحد) بررسي خواهد گرديد.
اكنون بر روي دكمه Save در اين صفحه كليك كرده و فايل نهايي را ذخيره كنيد (بعدا توسط دكمه Load در همين صفحه قابل بارگذاري خواهد بود). حاصل بايد به صورت زير باشد:
<PartCoverSettings>
<Target>D:\Prog\Libs\NUnit\bin\net-2.0\nunit-console.exe</Target>
<TargetWorkDir>D:\Prog\1390\TestPartCover\TestPartCover.Tests\bin\Debug</TargetWorkDir>
<TargetArgs>TestPartCover.Tests.dll /framework=4.0.30319 /noshadow</TargetArgs>
<Rule>+[TestPartCover]*</Rule>
<Rule>-[nunit*]*</Rule>
<Rule>-[log4net*]*</Rule>
</PartCoverSettings>

براي شروع به بررسي، بر روي دكمه Start كليك نمائيد. پس از مدتي، نتيجه به صورت زير خواهد بود:



بله! آزمون واحد تهيه شده تنها 39 درصد اسمبلي TestPartCover را پوشش داده است. مواردي كه با صفر درصد مشخص شده‌اند، يعني فاقد آزمون واحد هستند و نكته مهم‌تر پوشش 91 درصدي متد DoFoo است. براي اينكه علت را مشاهده كنيد از منوي View ، گزينه‌ي Coverage detail را انتخاب كنيد تا تصوير زير نمايان شود:



قسمت‌ نارنجي در اينجا به معناي عدم پوشش آن در متد TestDoFoo تهيه شده است. تنها قسمت‌هاي سبز را توانسته‌ايم پوشش دهيم و براي بررسي تمام شرط‌هاي اين متد نياز به آزمون‌هاي واحد بيشتري مي‌باشد.

روش نهايي كار نيز به همين صورت است. ابتدا آزمون واحد تهيه مي‌شود. سپس ميزان پوشش آن بررسي شده و در ادامه سعي خواهيم كرد تا اين درصد را افزايش دهيم.

۱۳۸۹/۱۱/۱۲

توليد خودكار آزمون‌هاي واحد NUnit


تعدادي ابزار براي توليد خودكار متدهاي آزمون‌هاي واحد NUnit از روي كلاس‌هاي موجود در يك اسمبلي وجود دارند كه به دو دسته تقسيم مي‌شود:

الف) آن‌هايي كه فقط نام كلاس‌هاي آزمون واحد و نام متدهاي آن‌را به صورت خودكار توليد مي‌كنند


اين ابزارها و كتابخانه‌ها، تنها كاري كه انجام مي‌دهند يافتن كلاس‌ها و متدهاي عمومي موجود در يك اسمبلي توسط Reflection و سپس توليد يك سري فايل‌ آماده از روي اين اطلاعات است. براي مثال اگر نام كلاس شما Class1 است فايلي به نام TestClass1 را توليد مي‌كنند و اگر يكي از متد‌هاي عمومي اين كلاس به نام Method1 باشد، يك متد خالي را به نام Method1Test ايجاد خواهند كرد و همين.
تبديل CodeSmith NUnit Test Generator فوق به يك T4 template كار ساده‌اي است.

ب) ابزارهايي كه علاوه بر مورد الف، سعي مي‌كنند بدنه‌اي را نيز براي متدهاي واحد توليد شده تهيه كنند


اين افزونه‌ها و برنامه‌ها سعي مي‌كنند به كمك Reflection و همچنين امكانات توليد كد موجود در VS.NET نسبت به توليد كلاس‌ها، متدها و بدنه‌هاي نمونه آن‌ها اقدام كنند. براي مثال اگر نام متد كلاسي، Method1 به همراه يك پارامتر از نوع int باشد، بدنه توليد شده به همراه وهله سازي از كلاس آن و فراخواني اين متد به همراه پارامتر آن خواهد بود.
مشكل مهم اين پروژه‌هاي سورس باز كوچك هم عدم تعهد به نگهداري آن‌ها است. براي مثال آخرين به روز رساني موجود افزونه‌ي NUnitGen شركت ناول، مخصوص VS2008 است يا آخرين به روز رساني TestGen.Net مربوط به دات نت يك است (سورسي هم كه در سايت سورس فورج قرار داده ناقص است) يا مقاله‌ي سايت CodeProject‌ كه ذكر گرديد، با نگارش‌هاي جديد NUnit درست كار نمي‌كند و كامپايل نمي‌شود.

در بين اين‌ها به نظر من Edwinyeah TestGen.Net كار جالبي را انجام داده است و چندين زبان را هم پشتيباني مي‌كند. البته همانطور كه عنوان شد توانايي بارگذاري اسمبلي‌هاي نگارش‌هاي جديد دات نت را ندارد كه موضوع مهمي نيست. سورس آن‌را مي‌توان دريافت و سپس جهت دات نت 4 كامپايل كرد. البته يك سري از كلاس‌هاي آن هم كه در سورس موجود نيستند را مي‌شود از اسمبلي كامپايل شده‌ي آن با Reflector درآورد، به پروژه اصلي اضافه و سپس كامپايل كرد!
كامپايل شده‌ي آن‌را جهت دات نت 4 از اينجا دريافت كنيد.

۱۳۸۸/۰۲/۱۷

آشنايي با mocking frameworks - قسمت دوم


استفاده از mocking frameworks :

تعدادي از چارچوب‌هاي تقليد نوشته شده براي دات نت فريم ورك مطابق ليست زير بوده و هدف از آن‌ها ايجاد ساده‌تر اشياء تقليد براي ما مي‌باشد:

Nmock : http://www.nmock.org
Moq : http://code.google.com/p/moq
Rhino Mocks : http://ayende.com/projects/rhino-mocks.aspx
TypeMock : http://www.typemock.com
EasyMock.Net : http://sourceforge.net/projects/easymocknet

در اين بين Rhino Mocks كه توسط يكي از اعضاي اصلي تيم NHibernate به وجود آمده است، در مجامع مرتبط بيشتر مورد توجه است. براي آشنايي بيشتر با آن مي‌توان به اين ويديوي رايگان آموزشي در مورد آن مراجعه نمود (حدود يك ساعت است).



خلاصه‌ا‌ي در مورد نحوه‌ي استفاده از Rhino Mocks :
پس از دريافت كتابخانه سورس باز Rhino Mocks ، ارجاعي را به اسمبلي Rhino.Mocks.dll آن، در پروژه آزمون واحد خود اضافه نمائيد.
يك Rhino mock test با ايجاد شيءايي از MockRepository شروع مي‌شود و كلا از سه قسمت تشكيل مي‌گردد:
الف) ايجاد شيء Mock يا Arrange . هدف از ايجاد شيء mock ، جايگزين كردن و يا تقليد يك شيء واقعي جهت مباحثي مانند ايزوله سازي آزمايشات، بالابردن سرعت آن‌ها و متكي به خود كردن اين آزمايشات مي‌باشد. همچنين در اين حالت نتايج false positive نيز كاهش مي‌يابند. منظور از نتايج false positive اين است كه آزمايش بايد با موفقيت به پايان برسد اما اينگونه نشده و علت آن بررسي سيستمي ديگر در خارج از مرزهاي سيستم فعلي است و مشكل از جاي ديگري نشات گرفته كه اساسا هدف از تست ما بررسي عملكرد آن سيستم نبوده است. كلا در اين موارد از mocking objects استفاده مي‌شود:
- دسترسي به شيء مورد نظر كند است مانند دسترسي به ديتابيس يا محاسبات بسيار طولاني
- شيء مورد نظر از call back استفاده مي‌كند
- شيء مورد آزمايش بايد به منابع خارجي دسترسي پيدا كند كه اكنون مهيا نيستند. براي مثال دسترسي به شبكه.
- شيءايي كه مي‌خواهيم آن‌را تست كنيم يا براي آن آزمايشات واحد تهيه نمائيم، هنوز كاملا توسعه نيافته و نيمه كاره است.
ب) تعريف رفتارهاي مورد نظر يا Act
ج) بررسي رفتارهاي تعريف شده يا Assert

مثال:
متد ساده زير را در نظر بگيريد:

public class ImageManagement
{
public string GetImageForTimeOfDay()
{
int currentHour = DateTime.Now.Hour;
return currentHour > 6 && currentHour < 21 ? "sun.jpg" : "moon.jpg";
}

}
آزمايش اين متد، وابسته است به زمان جاري سيستم.

using System;
using NUnit.Framework;

[TestFixture]
public class CMyTest
{
[Test]
public void DaytimeTest()
{
int currentHour = DateTime.Now.Hour;

if (currentHour > 6 && currentHour < 21)
{
const string expectedImagePath = "sun.jpg";
ImageManagement image = new ImageManagement();
string path = image.GetImageForTimeOfDay();
Assert.AreEqual(expectedImagePath, path);
}
else
{
Assert.Ignore("تنها در طول روز قابل بررسي است");
}
}

[Test]
public void NighttimeTest()
{
int currentHour = DateTime.Now.Hour;

if (currentHour < 6 || currentHour > 21)
{
const string expectedImagePath = "moon.jpg";
ImageManagement image = new ImageManagement();
string path = image.GetImageForTimeOfDay();
Assert.AreEqual(expectedImagePath, path);
}
else
{
Assert.Ignore("تنها در طول شب قابل بررسي است");
}
}

}
براي مثال اگر بخواهيم تصوير ماه را دريافت كنيم بايد تا ساعت 21 صبر كرد. همچنين بررسي اينكه چرا يكي از متدهاي آزمون واحد ما نيز با شكست مواجه شده است نيز نيازمند بررسي زمان جاري است و گاهي ممكن است با شكست مواجه شود و گاهي خير. در اين‌جا با استفاده از يك mock object ، اين وضعيت غيرقابل پيش بيني را با منطقي از پيش طراحي شده جايگزين كرده و آزمون خود را بر اساس آن انجام خواهيم داد.
براي اين‌كار بايد DateTime.Now.Hour را تقليد نموده و اينترفيسي را بر اساس آن طراحي نمائيم. سپس Rhino Mocks كار پياده سازي اين اينترفيس را انجام خواهد داد:

using NUnit.Framework;
using Rhino.Mocks;

namespace testWinForms87
{
public interface IDateTime
{
int GetHour();
}

public class ImageManagement
{
public string GetImageForTimeOfDay(IDateTime time)
{
int currentHour = time.GetHour();

return currentHour > 6 && currentHour < 21 ? "sun.jpg" : "moon.jpg";
}
}

[TestFixture]
public class CMocking
{
[Test]
public void DaytimeTest()
{
MockRepository mocks = new MockRepository();
IDateTime timeController = mocks.CreateMock<IDateTime>();

using (mocks.Record())
{
Expect.Call(timeController.GetHour()).Return(15);
}

using (mocks.Playback())
{
const string expectedImagePath = "sun.jpg";
ImageManagement image = new ImageManagement();
string path = image.GetImageForTimeOfDay(timeController);
Assert.AreEqual(expectedImagePath, path);
}
}

[Test]
public void NighttimeTest()
{
MockRepository mocks = new MockRepository();
IDateTime timeController = mocks.CreateMock<IDateTime>();
using (mocks.Record())
{
Expect.Call(timeController.GetHour()).Return(1);
}

using (mocks.Playback())
{
const string expectedImagePath = "moon.jpg";
ImageManagement image = new ImageManagement();
string path = image.GetImageForTimeOfDay(timeController);
Assert.AreEqual(expectedImagePath, path);
}
}
}

}
همانطور كه در ابتداي مطلب هم عنوان شد، mocking‌ از سه قسمت تشكيل مي‌شود:

MockRepository mocks = new MockRepository();
ابتدا شيء mocks را از MockRepository كتابخانه Rhino Mocks ايجاد مي‌كنيم تا بتوان از خواص و متدهاي آن استفاده كرد.
سپس اينترفيسي بايد به آن پاس شود تا انتظارات سيستم را بتوان در آن بر پا نمود:

IDateTime timeController = mocks.CreateMock<IDateTime>();
using (mocks.Record())
{
Expect.Call(timeController.GetHour()).Return(15);

}
به عبارت ديگر در اينجا به سيستم مقلد خود خواهيم گفت: زمانيكه شيء ساعت را تقليد كردي، لطفا عدد 15 را برگردان.
به اين صورت آزمايش ما بر اساس وضعيت مشخصي از سيستم صورت مي‌گيرد و وابسته به ساعت جاري سيستم نخواهد بود.

همانطور كه ملاحظه مي‌كنيد، روش Test Driven Development بر روي نحوه‌ي برنامه نويسي ما و ايجاد كلاس‌ها و اينترفيس‌هاي اوليه نيز تاثير زيادي خواهد گذاشت. استفاده از اينترفيس‌ها يكي از اصول پايه‌اي برنامه نويسي شيءگرا است و در اينجا مقيد به ايجاد آن‌ها خواهيم شد.

پس از آن‌كه در قسمت mocks.Record ، انتظارات خود را ثبت كرديم، اكنون نوبت به وضعيت Playback مي‌رسد:
using (mocks.Playback())
{
string expectedImagePath = "sun.jpg";
ImageManagement image = new ImageManagement();
string path = image.GetImageForTimeOfDay(timeController);
Assert.AreEqual(expectedImagePath, path);

}
در اينجا روش كار همانند ايجاد متدهاي آزمون واحد متداولي است كه تاكنون با آن‌ها آشنا شده‌ايم و تفاوتي ندارد.
با توجه به اينكه پس از تغيير طراحي متد GetImageForTimeOfDay ، اين متد اكنون از شيء IDateTime به عنوان ورودي استفاده مي‌كند، مي‌توان پياده سازي آن اينترفيس ‌را در آزمايشات واحد تقليد نمود و يا جايي كه قرار است در برنامه استفاده شود، مي‌تواند پياده سازي واقعي خود را داشته باشد و ديگر آزمايشات ما وابسته به آن نخواهد بود:

public class DateTimeController : IDateTime
{
public int GetHour()
{
return DateTime.Now.Hour;
}
}

۱۳۸۸/۰۲/۱۵

آشنايي با mocking frameworks (چارچوب‌هاي تقليد) - قسمت اول


اين مطلب در ادامه‌ي مطالب آزمو‌ن‌هاي واحد يا unit testing است.
نوشتن آزمون واحد براي كلاس‌هايي كه با يك سري از الگوريتم‌ها ، مسايل رياضي و امثال آن سر و كار دارند، ساده است. عموما اين نوع كلاس‌ها وابستگي خارجي آنچناني ندارند؛ اما در عمل كلاس‌هاي ما ممكن است وابستگي‌هاي خارجي بسياري پيدا كنند؛ براي مثال كار با ديتابيس، اتصال به يك وب سرويس، دريافت فايل از اينترنت، خواندن اطلاعات از انواع فايل‌ها و غيره.
مطابق اصول آزمايشات واحد، يك آزمون واحد خوب بايد ايزوله باشد. نبايد به مرزهاي سيستم‌هاي ديگر وارد شده و عملكرد سيستم‌هاي خارج از كلاس را بررسي كند.
اين مثال ساده را در نظر بگيريد:
فرض كنيد برنامه شما قرار است از يك وب سرويس ليستي از آدرس‌هاي IP يك كشور خاص را دريافت كند و در يك ديتابيس محلي آن‌ها را ذخيره نمايد. به صورت متداول اين كلاس بايد اتصالي را به وب سرويس گشوده و اطلاعات را دريافت كند و همچنين آن‌ها را خارج از مرز كلاس در يك ديتابيس ثبت كند. نوشتن آزمون واحد براي اين كلاس مطابق اصول مربوطه غير ممكن است. اگر كلاس آزمون واحد آن‌را تهيه نمائيد، اين آزمون، integration test نام خواهد گرفت زيرا از مرزهاي سيستم بايد عبور نمايد.

همچنين يك آزمون واحد بايد تا حد ممكن سريع باشد تا برنامه نويس از انجام آن بر روي يك پروژه بزرگ منصرف نگردد و ايجاد اين اتصالات در خارج از سيستم، بيشتر سبب كندي كار خواهند شد.

براي اين ايزوله سازي روش‌هاي مختلفي وجود دارند كه در ادامه به آن‌ها خواهيم پرداخت:

روش اول: استفاده از اينترفيس‌ها
با كمك يك اينترفيس مي‌توان مشخص كرد كه يك قطعه از كد "چه كاري" را قرار است انجام دهد؛ و نه اينكه "چگونه" بايد آن‌را به انجام رساند.
يك مثال ساده از خود دات نت فريم ورك، اينترفيس IComparable است:

public static string GetComparisonText(IComparable a, IComparable b)
{
if (a.CompareTo(b) == 1)
return "a is bigger";
if (a.CompareTo(b) == -1)
return "b is bigger";
return "same";
}
در اين مثال چون از IComparable استفاده شده، متد ما از هر نوع داده‌اي جهت مقايسه مي‌تواند استفاده كند. تنها موردي كه براي آن مهم خواهد بود اين است كه a راهي را براي مقايسه با b ارائه دهد.

اكنون با توجه به اين توضيحات، براي ايزوله كردن ارتباط با ديتابيس و وب سرويس در مثال فوق، مي‌توان اينترفيس‌هاي زير را تدارك ديد:
    public interface IEmailSource
{
IEnumerable<string> GetEmailAddresses();
}

public interface IEmailDataStore
{
void SaveEmailAddresses(IEnumerable<string> emailAddresses);

}
در اينجا استفاده و تعريف اينترفيس‌ها چندين خاصيت را به همراه خواهد داشت :
الف) به اين صورت تنها مشخص مي‌شود كه چه كاري را قصد داريم انجام دهيم و كاري به پياده سازي آن نداريم.
ب) ساخت كلاس بدون وجود يا دسترسي به يك ديتابيس ميسر مي‌شود. اين مورد خصوصا در يك كار تيمي كه قسمت‌هاي مختلف كار به صورت همزمان در حالت پيشرفت و تهيه است حائز اهميت مي‌شود.
ج) با توجه به اينكه در اينجا به پياده سازي توجهي نداريم، مي‌توان از اين اينترفيس‌ها جهت تقليد دنياي واقعي استفاده كنيم. (كه در اينجا mocking نام گرفته است)

جهت تقليد رفتار و عملكرد اين دو اينترفيس، به كلاس‌هاي تقليد زير خواهيم رسيد:

public class MockEmailSource : IEmailSource
{
public IEnumerable<string> EmailAddressesToReturn { get; set; }
public IEnumerable<string> GetEmailAddresses()
{
return EmailAddressesToReturn;
}
}

public class MockEmailDataStore : IEmailDataStore
{
public IEnumerable<string> SavedEmailAddresses { get; set; }
public void SaveEmailAddresses(IEnumerable<string> emailAddresses)
{
SavedEmailAddresses = emailAddresses;
}

}
تا اينجا اولين قدم در مورد ايزوله سازي كلاس‌هايي كه به مرز سيستم‌هاي ديگر وارد مي‌شوند، برداشته شد. اما به مرور زمان مديريت اين اينترفيس‌ها و افزودن رفتارهاي جديد به كلاس‌هاي مشتق شده از آن‌ها مشكل مي‌شود. به همين جهت تا حد ممكن از پياده سازي دستي آن‌ها خودداري شده و روش پيشنهادي استفاده از mocking frameworks است.

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

۱۳۸۸/۰۱/۱۵

نگارش نهايي MBUnit 3 ارائه شد


MBUnit كه يكي ديگر از فريم ورك‌هاي آزمايش واحد يا unit testing دات نت به شمار مي‌رود، نگارش 3 بتا آن از سال 2007 شروع شده و اخيرا نگارش نهايي 3 آن ارائه گرديده است.


جهت مشاهده‌ي جزئيات آخرين تغييرات اعمال شده در نگارش جديد آن، MbUnit v3.0.6 Update 1 مي‌توان به وبلاگ يكي از اعضاي اصلي تيم مراجعه نمود.

Added support for TestDriven.Net category filters.
Added support for more powerful inclusion/exclusion test filter expressions.
Fixed ReSharper v3.1, v4.0 and v4.1 hangs.
Increased NCover v1.5.8 timeout.
Adopted new assembly version numbering scheme

MBUnit از جهات بسياري از NUnit پيشرفته‌تر است براي مثال در هنگام انجام آزمايشات واحد بر روي يك ديتابيس، به صورت خودكار امكان حذف ركوردهاي آزمايشي را داشته و كار به حالت اول بازگردان وضعيت ديتابيس را انجام مي‌دهد.
همچنين نگارش 3 آن كار يكپارچه شدن با ReSharper را هم انجام مي‌دهد كه پيشتر به صورت پيش فرض مهيا نبود و اين مورد يكي از دلايل مهم استفاده گسترده از NUnit به شمار مي‌رفت.

پ.ن.
براي دريافت آن بايد به گوگل‌كد مراجعه كرد كه احتمالا با مشكلاتي همراه خواهد بود. نگارش نهايي آن تا اين تاريخ را از اينجا دريافت كنيد.

۱۳۸۸/۰۱/۱۱

قالبي براي ايجاد آزمون‌هاي NUnit مخصوص ReSharper


افزونه‌ي ReSharper به‌دليل يكپارچه كردن امكان استفاده از NUnit در ويژوال استوديو، يكي از انتخاب‌هاي اول جهت انجام آزمايشات واحد در اين محيط به شمار مي‌رود.
اخيرا آقاي Genisio چند قالب ايجاد آزمون‌هاي NUnit را مخصوص ReSharper ايجاد كرده‌اند، كه در ادامه در مورد نحوه‌ي استفاده از آن‌ها توضيح داده خواهد شد.
پس از دريافت فايل‌ها، براي استفاده، به منوي ReSharper گزينه‌ي live templates مراجعه نمائيد. سپس بر روي نوار ابزار صفحه‌ي باز شده، روي دكمه‌ي import كليك نموده و فايل‌ها را معرفي كنيد.
NewTestFileTemplate.xml از نوع file template است.
TestTemplates.xml از نوع live template مي‌باشد.


اكنون مجددا به منوي اصلي ReSharper مراجعه كنيد و مسير زير را طي نمائيد:

ReSharper -> new from template -> more …




گزينه‌ي Test اضافه شده را انتخاب كرده و سپس قسمت Add to quicklist را نيز انتخاب نمائيد.
به اين صورت گزينه‌ي Test به اين منو افزوده خواهد شد و هر بار كه بر روي آن كليك شود، يك كلاس حاضر و آماده مطابق قالب اصلي يك كلاس استاندارد NUnit براي شما ايجاد خواهد شد.
همچنين در اين مجموعه يك سري live template نيز موجود است كه كار آن‌ها فعال سازي intellisense ويژوال استوديو جهت ايجاد يك سري متدها به صورت خودكار است. براي مثال اگر كلمه‌ي test را تايپ كنيد و سپس دكمه‌ي tab و يا enter را فشار دهيد، بلافاصله بدنه‌ي خالي يك متد تست براي شما ايجاد خواهد شد.
ساير ميان‌بر‌هاي در نظر گرفته شده، به شرح زير هستند:
test – Create a new [Test] method
setup – Create a [SetUp] method
teardown – Create a new [TearDown] method
ise – Assert that condition is equal to value
ist – Assert that condition is true
isf – Assert that condition is false
isn – Assert that condition is null
isnn – Assert that condition is not null


۱۳۸۷/۱۱/۰۷

تعيين اعتبار يك GUID در دات نت


GUID يا Globally unique identifier يك عدد صحيح 128 بيتي است (بنابراين 2 به توان 128 حالت را مي‌توان براي آن درنظر گرفت). از لحاظ آماري توليد دو GUID يكسان تقريبا صفر مي‌باشد. به همين جهت از آن با اطمينان مي‌توان به عنوان يك شناسه منحصربفرد استفاده كرد. براي مثال اگر به لينك‌هاي دانلود فايل‌ها از سايت مايكروسافت دقت كنيد، اين نوع GUID ها را به وفور مي‌توانيد ملاحظه نمائيد. يا زمانيكه قرار است فايلي را كه بر روي سرور آپلود شده، ذخيره نمائيم، مي‌توان نام آن‌را يك GUID درنظر گرفت بدون اينكه نگران باشيم آيا فايل آپلود شده بر روي يكي از فايل‌هاي موجود overwrite مي‌شود يا خير. يا مثلا استفاده از آن در سناريوي بازيابي كلمه عبور در يك سايت. هنگاميكه كاربري درخواست بازيابي كلمه عبور فراموش شده خود را داد، يك GUID براي آن توليد كرده و به او ايميل مي‌زنيم و در آخر آن‌را در كوئري استرينگي دريافت كرده و با مقدار موجود در ديتابيس مقايسه مي‌كنيم. مطمئن هستيم كه اين عبارت قابل حدس زدن نيست و همچنين يكتا است.

براي توليد GUID ها در دات نت مي‌توان مانند مثال زير عمل كرد و خروجي‌هاي دلخواهي را با فرمت‌هاي مختلفي دريافت كرد:

System.Guid.NewGuid().ToString() = 81276701-9dd7-42e9-b128-81c762a172ff
System.Guid.NewGuid().ToString("N") = 489ecfc61ee7403988efe8546806c6a2
System.Guid.NewGuid().ToString("D") = 119201d9-84d9-4126-b93f-be6576eedbfd
System.Guid.NewGuid().ToString("B") = {fd508d4b-cbaf-4f1c-894c-810169b1d20c}
System.Guid.NewGuid().ToString("P") = (eee1fe00-7e63-4632-a290-516bfc457f42)

تمام اين‌ها خيلي هم خوب! اما همان سناريوي مشخص ساختن يك فايل با GUID و يا بازيابي كلمه عبور فراموش شده را درنظر بگيريد. يكي از اصول امنيتي مهم، تعيين اعتبار ورودي كاربر است. چگونه بايد يك GUID را به صورت مؤثري تعيين اعتبار كرد و مطمئن شد كه كاربر از اين راه قصد تزريق اس كيوال را ندارد؟
دو روش براي انجام اينكار وجود دارد
الف) عبارت دريافت شده را به new Guid پاس كنيم. اگر ورودي غيرمعتبر باشد، يك exception توليد خواهد شد.
ب) استفاده از regular expressions جهت بررسي الگوي عبارت وارد شده

پياده سازي اين دو را در كلاس زير مي‌توان ملاحظه نمود:
using System;
using System.Text.RegularExpressions;

namespace sample
{
/// <summary>
/// بررسي اعتبار يك گوئيد
/// </summary>
public static class CValidGUID
{
/// <summary>
/// بررسي تعيين اعتبار ورودي
/// </summary>
/// <param name="guidString">ورودي</param>
/// <returns></returns>
public static bool IsGuid(this string guidString)
{
if (string.IsNullOrEmpty(guidString)) return false;

bool bResult;
try
{
Guid g = new Guid(guidString);
bResult = true;
}
catch
{
bResult = false;
}

return bResult;
}

/// <summary>
/// بررسي تعيين اعتبار ورودي
/// </summary>
/// <param name="input">ورودي</param>
/// <returns></returns>
public static bool IsValidGUID(this string input)
{
return !string.IsNullOrEmpty(input) &&
new Regex(@"^(\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1})$").IsMatch(input);
}
}

}

سؤال: آيا متدهاي فوق ( extension methods ) درست كار مي‌كنند و واقعا نياز ما را برآورده خواهند ساخت؟ به همين منظور، آزمايش واحد آن‌ها را نيز تهيه خواهيم كرد:

using NUnit.Framework;
using sample;

namespace TestLibrary
{
[TestFixture]
public class TestCValidGUID
{

/*******************************************************************************/
[Test]
public void TestIsGuid1()
{
Assert.IsTrue("81276701-9dd7-42e9-b128-81c762a172ff".IsGuid());
}

[Test]
public void TestIsGuid2()
{
Assert.IsTrue("489ecfc61ee7403988efe8546806c6a2".IsGuid());
}

[Test]
public void TestIsGuid3()
{
Assert.IsTrue("{fd508d4b-cbaf-4f1c-894c-810169b1d20c}".IsGuid());
}

[Test]
public void TestIsGuid4()
{
Assert.IsTrue("(eee1fe00-7e63-4632-a290-516bfc457f42)".IsGuid());
}

[Test]
public void TestIsGuid5()
{
Assert.IsFalse("81276701;9dd7;42e9-b128-81c762a172ff".IsGuid());
}


/*******************************************************************************/
[Test]
public void TestIsValidGUID1()
{
Assert.IsTrue("81276701-9dd7-42e9-b128-81c762a172ff".IsValidGUID());
}

[Test]
public void TestIsValidGUID2()
{
Assert.IsTrue("489ecfc61ee7403988efe8546806c6a2".IsValidGUID());
}

[Test]
public void TestIsValidGUID3()
{
Assert.IsTrue("{fd508d4b-cbaf-4f1c-894c-810169b1d20c}".IsValidGUID());
}

[Test]
public void TestIsValidGUID4()
{
Assert.IsTrue("(eee1fe00-7e63-4632-a290-516bfc457f42)".IsValidGUID());
}

[Test]
public void TestIsValidGUID5()
{
Assert.IsFalse("81276701;9dd7;42e9-b128-81c762a172ff".IsValidGUID());
}
}

}

نتيجه اين آزمايش به صورت زير است:



همانطور كه ملاحظه مي‌كنيد حالت دوم يعني استفاده از عبارات باقاعده دو حالت را نمي‌تواند بررسي كند (مطابق الگوي بكار گرفته شده كه البته قابل اصلاح است)، اما روش معمولي استفاده از new Guid ، تمام فرمت‌هاي توليد شده توسط دات نت را پوشش مي‌دهد.


۱۳۸۷/۱۰/۲۱

آشنايي با آزمايش واحد (unit testing) در دات نت، قسمت 6


ادامه آشنايي با NUnit

فرض كنيد يك RSS reader نوشته‌ايد كه فيدهاي فارسي و انگليسي را دريافت مي‌كند. به صورت پيش فرض هم مشخص نيست كه كدام فيد اطلاعات فارسي را ارائه خواهد داد و كداميك انگليسي. تشخيص محتواي فارسي و از راست به چپ نشان دادن خودكار مطالب ‌آن‌ها به عهده‌ي برنامه نويس است. بهترين روش براي تشخيص اين نوع الگوها، استفاده از regular expressions است.
براي مثال الگوي تشخيص اينكه آيا متن ما حاوي حروف انگليسي است يا خير به صورت زير است:

[a-zA-Z]

كه بيان بصري آن به اين شكل است.
در مورد تشخيص وجود حروف فارسي در يك عبارت، يكي از دو الگوي زير بكار مي‌رود:

[\u0600-\u06FF]
[ا-یءئ]

در مورد اينكه بازه يونيكد فارسي استاندارد از كجا شروع مي‌شود مي‌توان به مقاله‌ي آقاي حاج‌لو مراجعه نمود (به صورت خلاصه، بازه مصوب عربي يونيكد، همان بازه يونيكد فارسي نيز مي‌باشد. يا به بيان بهتر، بازه‌ي فارسي، جزئي از بازه‌اي است كه عربي نام گرفته است). البته بازه‌ي مصوب ديگري هم در مورد ايران باستان وجود دارد به نام old Persian كه مورد استفاده‌ي روزمره‌اي ندارد!

كلاس زير را در مورد استفاده از اين الگوها تهيه كرده‌ايم:

using System.Text.RegularExpressions;

namespace sample
{
public static class CDetectFarsi
{
public static bool ContainsFarsiData(this string txt)
{
return !string.IsNullOrEmpty(txt) &&
Regex.IsMatch(txt, "[ا-یءئ]");
}

public static bool ContainsFarsi(this string txt)
{
return !string.IsNullOrEmpty(txt) &&
Regex.IsMatch(txt, @"[\u0600-\u06FF]");
}
}
}

همانطور كه ملاحظه مي‌كنيد در اينجا از extension methods سي شارپ 3 جهت توسعه كلاس پايه string استفاده شد.
اكنون مي‌خواهيم بررسي كنيم آيا اين الگوها مقصود ما را برآورده مي‌سازند يا خير.

using NUnit.Framework;
using sample;

namespace TestLibrary
{
[TestFixture]
public class TestFarsiClass
{
/*******************************************************************************/
[Test]
public void TestContainsFarsi1()
{
Assert.IsTrue("وحيد".ContainsFarsi());
}

[Test]
public void TestContainsFarsi2()
{
Assert.IsTrue("گردان".ContainsFarsi());
}

[Test]
public void TestContainsFarsi3()
{
Assert.IsTrue("سپيدTest".ContainsFarsi());
}

[Test]
public void TestContainsFarsi4()
{
Assert.IsTrue("123بررسي456".ContainsFarsi());
}

[Test]
public void TestContainsFarsi5()
{
Assert.IsFalse("Book".ContainsFarsi());
}

[Test]
public void TestContainsFarsi6()
{
Assert.IsTrue("۱۳۸۷".ContainsFarsi());
}

[Test]
public void TestContainsFarsi7()
{
Assert.IsFalse("Здравствуйте!".ContainsFarsi()); //Russian hello!
}


/*******************************************************************************/
[Test]
public void TestContainsFarsiData1()
{
Assert.IsTrue("وحيد".ContainsFarsiData());
}

[Test]
public void TestContainsFarsiData2()
{
Assert.IsTrue("گردان".ContainsFarsiData());
}

[Test]
public void TestContainsFarsiData3()
{
Assert.IsTrue("سپيدTest".ContainsFarsiData());
}

[Test]
public void TestContainsFarsiData4()
{
Assert.IsTrue("123بررسي456".ContainsFarsiData());
}

[Test]
public void TestContainsFarsiData5()
{
Assert.IsFalse("Book".ContainsFarsiData());
}

[Test]
public void TestContainsFarsiData6()
{
Assert.IsTrue("۱۳۸۷".ContainsFarsiData());
}

[Test]
public void TestContainsFarsiData7()
{
Assert.IsFalse("Здравствуйте!".ContainsFarsiData()); //Russian hello!
}
}
}

در كلاس فوق هر دو متد را با آزمايش‌هاي واحد مختلفي بررسي كرده‌ايم، انواع و اقسام حروف فارسي، تركيبي از فارسي و انگليسي، تركيبي از فارسي و اعداد انگليسي، عبارت كاملا انگليسي، عدد كاملا فارسي و يك عبارت روسي! (در يك كلاس عمومي با متدهاي عمومي بدون آرگومان از نوع void)
كلاس CDetectFarsi در برنامه اصلي قرار دارد و كلاس TestFarsiClass در يك پروژه class library ديگر قرار گرفته است (در اين مورد و جدا سازي آزمايش‌ها از پروژه اصلي در قسمت‌هاي قبل بحث شد)
همچنين به ازاي هر عبارت Assert يك متد ايجاد گرديد تا شكست يكي، سبب اختلال در بررسي ساير موارد نشود.
نتيجه اجراي اين آزمايش واحد با استفاده از امكانات مجتمع افزونه ReSharper به صورت زير است:



منهاي يك مورد، ساير آزمايشات ما با موفقيت انجام شده‌اند. موردي كه با شكست مواجه شده، بررسي اعداد كاملا فارسي است كه البته در الگوي دوم لحاظ نشده است و انتظار هم نمي‌رود كه آن‌را به اين شكل پشتيباني كند.
براي اينكه در حين اجراي آزمايشات بعدي اين متد در نظر گرفته نشود، مي‌توان ويژگي Test آن‌را به صورت زير تغيير داد:

[Test,Ignore]

نكته: مرسوم شده است كه نام متدهاي آزمايش واحد به صورت زير تعريف شوند (با Test شروع شوند، در ادامه نام متدي كه بررسي مي‌شود ذكر گردد و در آخر ويژگي مورد بررسي عنوان شود):

Test[MethodToBeTested][SomeAttribute]

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

۱۳۸۷/۱۰/۱۸

آشنايي با آزمايش واحد (unit testing) در دات نت، قسمت 5


ادامه آشنايي با NUnit

حالت‌هاي مختلف Assert :

NUnit framework حالت‌هاي مختلفي از دستور Assert را پشتيباني مي‌كند كه در ادامه با آنها آشنا خواهيم شد.

كلاس Assertion :
اين كلاس داراي متدهاي زير است:
public static void Assert(bool condition)
public static void Assert(string message, bool condition)

تنها در حالتي اين بررسي موفقيت آميز گزارش خواهد شد كه condition مساوي true باشد
public static void AssertEquals(string message, object expected, object actual)
public static void AssertEquals(string message, float expected, float actual, float delta)
public static void AssertEquals(string message, double expected, double actual, double delta)
public static void AssertEquals(string message, int expected, int actual)
public static void AssertEquals(int expected, int actual)
public static void AssertEquals(object expected, object actual)
public static void AssertEquals(float expected, float actual, float delta)
public static void AssertEquals(double expected, double actual, double delta)

تنها در صورتي اين بررسي به اثبات خواهد رسيد كه اشياء actual و expected يكسان باشند. (دلتا در اينجا به عنوان تلرانس آزمايش درنظر گرفته مي‌شود)

public static void AssertNotNull(string message, object anObject)
public static void AssertNotNull(object anObject)

اين بررسي تنها در صورتي موفقيت آميز گزارش مي‌شود كه شيء مورد نظر نال نباشد.

public static void AssertNull(string message, object anObject)
public static void AssertNull(object anObject)

اين بررسي تنها در صورتي موفقيت آميز گزارش مي‌شود كه شيء مورد نظر نال باشد.

public static void AssertSame(string message, object expected, object actual)
public static void AssertSame(object expected, object actual)

تنها در صورتي اين بررسي به اثبات خواهد رسيد كه اشياء actual و expected يكسان باشند.

public static void Fail(string message)
public static void Fail()

همواره Fail خواهد شد. (در مورد كاربرد آن در قسمت بعد توضيح داده خواهد شد)


نكته:
در يك متد آزمايش واحد شما مجازيد به هرتعدادي كه لازم است از متدهاي Assertion استفاده نمائيد. در اين حالت اگر تنها يكي از متدهاي assertion با شكست روبرو شود، كل متد آزمايش واحد شما مردود گزارش شده و همچنين عبارات بعدي Assertion بررسي نخواهند شد. بنابراين توصيه مي‌شود به ازاي هر متد آزمايش واحد، تنها از يك Assertion استفاده نمائيد.

مهم!
كلاس Assertion منسوخ شده است و توصيه مي‌شود بجاي آن از كلاس Assert استفاده گردد.

آشنايي با كلاس Assert :
اين كلاس از متدهاي زير تشكيل شده است:

الف) بررسي حالت‌هاي تساوي

Assert.AreEqual( object expected, object actual );

جهت بررسي تساوي دو شيء مورد بررسي و شيء مورد انتظار بكار مي‌رود.

Assert.AreNotEqual( object expected, object actual );

جهت بررسي عدم تساوي دو شيء مورد بررسي و شيء مورد انتظار بكار مي‌رود.
براي مشاهده انواع و اقسام overload هاي آن‌ها مي‌توانيد به راهنماي NUnit كه پس از نصب، در پوشه doc آن قرار مي‌گيرد مراجعه نمائيد.

همچنين دو متد زير و انواع overload هاي آن‌ها جهت برسي اختصاصي حالت تساوي دو شيء بكار مي‌روند:

Assert.AreSame( object expected, object actual );
Assert.AreNotSame( object expected, object actual );

بعلاوه اگر نياز بود بررسي كنيم كه آيا شيء مورد نظر حاوي يك آرايه يا ليست بخصوصي است مي‌توان از متد زير و oveload هاي آن استفاده نمود:

Assert.Contains( object anObject, IList collection );

ب) بررسي حالت‌هاي شرطي:

Assert.IsTrue( bool condition );

تنها در حالتي اين بررسي موفقيت آميز گزارش خواهد شد كه condition مساوي true باشد

Assert.IsFalse( bool condition);

تنها در حالتي اين بررسي موفقيت آميز گزارش خواهد شد كه condition مساوي false باشد
Assert.IsNull( object anObject );

اين بررسي تنها در صورتي موفقيت آميز گزارش مي‌شود كه شيء مورد نظر نال باشد.
Assert.IsNotNull( object anObject );

اين بررسي تنها در صورتي موفقيت آميز گزارش مي‌شود كه شيء مورد نظر نال نباشد.

Assert.IsNaN( double aDouble );

اين بررسي تنها در صورتي موفقيت آميز گزارش مي‌شود كه شيء مورد نظر عددي نباشد (اگر با جاوا اسكريپت كار كرده باشيد حتما با isNan آشنا هستيد، is not a numeric ).

Assert.IsEmpty( string aString );
Assert.IsEmpty( ICollection collection );

جهت بررسي خالي بودن يك رشته يا ليست بكار مي‌رود.

Assert.IsNotEmpty( string aString );
Assert.IsNotEmpty( ICollection collection );

جهت بررسي خالي نبودن يك رشته يا ليست بكار مي‌رود.

ج) بررسي حالت‌هاي مقايسه‌اي

Assert.Greater( double arg1, double arg2 );
Assert.GreaterOrEqual( int arg1, int arg2 );
Assert.Less( int arg1, int arg2 );
Assert.LessOrEqual( int arg1, int arg2 );

نكته‌اي را كه در اينجا بايد در نظر داشت اين است كه همواره شيء اول با شيء دوم مقايسه مي‌شود. مثلا در حالت اول، اگر شيء اول بزرگتر از شيء دوم بود، آزمايش مورد نظر با موفقيت گزارش خواهد شد.
از ذكر انواع و اقسام overload هاي اين توابع جهت طولاني نشدن مطلب پرهيز شد.

د) بررسي نوع اشياء

Assert.IsInstanceOfType( Type expected, object actual );
Assert.IsNotInstanceOfType( Type expected, object actual );
Assert.IsAssignableFrom( Type expected, object actual );
Assert.IsNotAssignableFrom( Type expected, object actual );

اين توابع و Overload هاي آن‌ها امكان بررسي نوع شيء مورد نظر را ميسر مي‌سازند.

ه) متدهاي كمكي
Assert.Fail();
Assert.Ignore();

در حالت استفاده از ignore ، آزمايش واحد شما در حين اجرا نديد گرفته خواهد شد. از متد fail براي طراحي يك متد assertion سفارشي مي‌توان استفاده كرد. براي مثال:
طراحي متدي كه بررسي كند آيا يك رشته مورد نظر حاوي عبارتي خاص مي‌باشد يا خير:

public void AssertStringContains( string expected, string actual,
string message )
{
if ( actual.IndexOf( expected ) < 0 )
Assert.Fail( message );
}

و) متدهاي ويژه‌ي بررسي رشته‌ها

StringAssert.Contains( string expected, string actual );
StringAssert.StartsWith( string expected, string actual );
StringAssert.EndsWith( string expected, string actual );
StringAssert.AreEqualIgnoringCase( string expected, string actual );
StringAssert.IsMatch( string expected, string actual );

اين متدها و انواع overload هاي آن‌ها جهت بررسي‌هاي ويژه رشته‌ها بكار مي‌روند. براي مثال آيا رشته مورد نظر حاوي عبارتي خاص است؟ آيا با عبارتي خاص شروع مي‌شود يا با عبارتي ويژه، پايان مي‌پذيرد و امثال آن.

ز) بررسي فايل‌ها

FileAssert.AreEqual( Stream expected, Stream actual );
FileAssert.AreEqual( FileInfo expected, FileInfo actual );
FileAssert.AreEqual( string expected, string actual );

FileAssert.AreNotEqual( Stream expected, Stream actual );
FileAssert.AreNotEqual( FileInfo expected, FileInfo actual );
FileAssert.AreNotEqual( string expected, string actual );

اين متدها جهت مقايسه دو فايل بكار مي‌روند و ورودي‌هاي آن‌ها مي‌تواند از نوع stream ، شيء FileInfo و يا مسير فايل‌ها باشد.

ح) بررسي collections

CollectionAssert.AllItemsAreInstancesOfType( IEnumerable collection, Type expectedType );
CollectionAssert.AllItemsAreNotNull( IEnumerable collection );
CollectionAssert.AllItemsAreUnique( IEnumerable collection );
CollectionAssert.AreEqual( IEnumerable expected, IEnumerable actual );
CollectionAssert.AreEquivalent( IEnumerable expected, IEnumerable actual);
CollectionAssert.AreNotEqual( IEnumerable expected, IEnumerable actual );
CollectionAssert.AreNotEquivalent( IEnumerable expected,IEnumerable actual );
CollectionAssert.Contains( IEnumerable expected, object actual );
CollectionAssert.DoesNotContain( IEnumerable expected, object actual );
CollectionAssert.IsSubsetOf( IEnumerable subset, IEnumerable superset );
CollectionAssert.IsNotSubsetOf( IEnumerable subset, IEnumerable superset);
CollectionAssert.IsEmpty( IEnumerable collection );
CollectionAssert.IsNotEmpty( IEnumerable collection );

به صورت اختصاصي و ويژه نيز مي‌توان بررسي مقايسه‌اي را بر روي اشيايي از نوع IEnumerable انجام داد. براي مثال آيا معادل هستند، آيا شيء مورد نظر نال نيست و امثال آن.

نكته: در تمامي overload هاي اين توابع، آرگومان message نيز وجود دارد. از اين آرگومان زمانيكه آزمايش با شكست مواجه شد، جهت ارائه اطلاعات بيشتري استفاده مي‌گردد.

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

۱۳۸۷/۱۰/۱۳

آشنايي با آزمايش واحد (unit testing) در دات نت، قسمت 4


ادامه آشنايي با NUnit

اگر قسمت سوم را دنبال كرده باشيد احتمالا از تعداد مراحلي كه بايد در خارج از IDE صورت گيرد گلايه خواهيد كرد (كامپايل كن، اجرا كن، اتچ كن، باز كن، ذخيره كن، اجرا كن و ...). خوشبختانه افزونه ReSharper اين مراحل را بسيار ساده و مجتمع كرده است.
اين افزونه به صورت خودكار متدهاي آزمايش واحد يك پروژه را تشخيص داده و آنها را با آيكون‌هايي ( Gutter icons ) متمايز مشخص مي‌سازد (شكل زير). پس از كليك بر روي آن‌ها ، امكان اجراي آزمايش يا حتي ديباگ كردن سطر به سطر يك متد آزمايش واحد درون IDE‌ ويژوال استوديو وجود خواهد داشت.



براي نمونه پس از اجراي آزمايش واحد قسمت قبل، نتيجه حاصل مانند شكل زير خواهد بود:



راه ديگر، استفاده از افزونه TestDriven.NET است كه نحوه استفاده از آن‌را اينجا مي‌توانيد ملاحظه نمائيد. به منوي جهنده كليك راست بر روي يك صفحه، گزينه run tests را اضافه مي‌كند و نتيجه حاصل را در پنجره output ويژوال استوديو نمايش مي‌دهد.

ساختار كلي يك كلاس آزمايش واحد مبتني بر NUnit framework :

using NUnit.Framework;
using NUnit.Framework.SyntaxHelpers;

namespace TestLibrary
{
[TestFixture]
public class Test2
{
[SetUp]
public void MyInit()
{
//كدي كه در اين قسمت قرار مي‌گيرد پيش از اجراي هر متد تستي اجرا خواهد شد
}

[TearDown]
public void MyClean()
{
//كدي كه در اين قسمت قرار مي‌گيرد پس از اجراي هر متد تستي اجرا خواهد شد
}

[TestFixtureSetUp]
public void MyTestFixtureSetUp()
{
// كدي كه در اينجا قرار مي‌گيرد در ابتداي بررسي آزمايش واحد و فقط يكبار اجرا مي‌شود
}

[TestFixtureTearDown]
public void MyTestFixtureTearDown()
{
// كدهاي اين قسمت در پايان كار يك كلاس آزمايش واحد اجرا خواهند شد
}

[Test]
public void Test1()
{
//بدنه آزمايش واحد در اينجا قرار مي‌گيرد
Assert.That(2, Is.EqualTo(2));
}
}
}

شبيه به روال‌هاي رخداد گردان load و close يك فرم، يك كلاس آزمايش واحد NUnit نيز داراي ويژگي‌هاي TestFixtureSetUp و TestFixtureTearDown است كه در ابتدا و انتهاي آزمايش واحد اجرا خواهند شد (براي درك بهتر موضوع و دنبال كردن نحوه‌ي اجراي اين روال‌ها، داخل اين توابع break point قرار دهيد و با استفاده از ReSharper ، آزمايش را در حالت ديباگ آغاز كنيد)، يا SetUp و TearDown كه در زمان آغاز و پايان بررسي هر متد آزمايش واحدي فراخواني مي‌شوند.
همانطور كه در قسمت قبل نيز ذكر شد، به امضاهاي متدها و كلاس فوق دقت نمائيد (عمومي ، void و بدون آرگومان ورودي).
بهتر است از ويژگي‌هاي SetUp و TearDown با دقت استفاده نمود. عموما هدف از اين روال‌ها ايجاد يك شيء و تخريب و پاك سازي آن است. حال اينكه اين روال‌ها قبل و پس از اجراي هر متد آزمايش واحدي فراخواني مي‌شوند. بنابراين به اين موضوع دقت داشته باشيد.
همچنين توصيه مي‌شود كه كلاس‌هاي آزمايش واحد را در اسمبلي ديگري مجزا از پروژه اصلي پياده سازي كنيد (براي مثال يك پروژه جديد از نوع class library)، زيرا اين موارد مرتبط با بررسي كيفيت كدهاي شما هستند كه موضوع جداگانه‌اي نسبت به پروژه اصلي محسوب مي‌گردد (نحوه پياده سازي آن‌‌را در قسمت قبل ملاحظه نموديد). همچنين در يك پروژه تيمي اين جدا سازي، مديريت آزمايشات را ساده‌تر مي‌سازد و بعلاوه سبب حجيم شدن بي‌مورد اسمبلي‌هاي اصلي محصول شما نيز نمي‌گردند.


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

۱۳۸۷/۱۰/۰۹

آشنايي با آزمايش واحد (unit testing) در دات نت، قسمت 3


آشنايي با NUnit

NUnit يكي از فريم ورك‌هاي آزمايش واحد سورس باز مخصوص دات نت فريم ورك است. (كلا در دات نت هرجايي ديديد كه N ، به ابتداي برنامه‌اي يا كتابخانه‌اي اضافه شده يعني نمونه منتقل شده از محيط جاوا به دات نت است. براي مثال NHibernate از Hibernate جاوا گرفته شده است و الي آخر)
اين برنامه با سي شارپ نوشته شده است اما تمامي زبان‌هاي دات نتي را پشتيباني مي‌كند (اساسا با زبان نوشته شده كاري ندارد و فايل اسمبلي برنامه را آناليز مي‌كند. بنابراين فرقي نمي‌كند كه در اينجا چه زباني بكار گرفته شده است).

ابتدا NUnit را دريافت نمائيد:
http://nunit.org/index.php?p=download

يك برنامه ساده از نوع console را در VS.net آغاز كنيد.
كلاس MyList را با محتواي زير به پروژه اضافه كنيد:
using System.Collections.Generic;

namespace sample
{
public class MyList
{
public static List<int> GetListOfIntItems(int numberOfItems)
{
List<int> res = new List<int>();

for (int i = 0; i < numberOfItems; i++)
res.Add(i);

return res;
}
}

}

يكبار پروژه را كامپايل كنيد.

اكنون بر روي نام پروژه در قسمت solution explorer كليك راست كرده و گزينه add->new project را انتخاب كنيد. نوع اين پروژه را كه متدهاي آزمايش واحد ما را تشكيل خواهد داد، class library انتخاب كنيد. با نام مثلا TestLibrary (شكل زير).



با توجه به اينكه NUnit ، اسمبلي برنامه (فايل exe يا dll آن‌را) آناليز مي‌كند، بنابراين مي‌توان پروژه تست را جداي از پروژه اصلي ايجاد نمود و مورد استفاده قرار داد.
پس از ايجاد پروژه class library ، بايد ارجاعي از NUnit framework را به آن اضافه كنيم. به محل نصب NUnit مراجعه كرده (پوشه bin آن) و ارجاعي به فايل nunit.framework.dll را به پروژه اضافه نمائيد (شكل زير).



سپس فضاهاي نام مربوطه را به كلاس آزمايش واحد خود اضافه خواهيم كرد:

using NUnit.Framework;
using NUnit.Framework.SyntaxHelpers;

اولين نكته‌اي را كه بايد در نظر داشت اين است كه كلاس آزمايش واحد ما بايد Public باشد تا در حين آناليز اسمبلي پروژه توسط NUint، قابل دسترسي و بررسي باشد.
سپس بايد ويژگي جديدي به نام TestFixture را به اين كلاس اضافه كرد.

[TestFixture]
public class TestClass

اين ويژگي به NUnit‌ مي‌گويد كه در اين كلاس به دنبال متدهاي آزمايش واحد بگرد. (در NUnit از attribute ها براي توصيف عملكرد يك متد و همچنين دسترسي runtime به آن‌ها استفاده مي‌شود)
سپس هر متدي كه به عنوان متد آزمايش واحد نوشته مي‌شود، بايد داراي ويژگي Test باشد تا توسط NUnit بررسي گردد:

[Test]
public void TestGetListOfIntItems()

نكته: متد Test ما بايد public‌ و از نوع void باشد و همچنين هيچ پارامتري هم نبايد داشته باشد.

اكنون براي اينكه بتوانيم متد GetListOfIntItems برنامه خود را در پروژه ديگري تست كنيم، بايد ارجاعي را به اسمبلي آن اضافه كنيم. همانند قبل، از منوي project‌ گزينه add reference ، فايل exe برنامه كنسول خود را انتخاب كرده و ارجاعي از آن‌را به پروژه class library اضافه مي‌كنيم. بديهي است امكان اينكه كلاس تست در همان پروژه هم قرار مي‌گرفت وجود داشت و صرفا جهت جداسازي آزمايش از برنامه اصلي اين‌كار صورت گرفت.
پس از اين مقدمات، اكنون متد آزمايش واحد ساده زير را در نظر بگيريد:

[Test]
public void TestGetListOfIntItems()
{
const int count = 5;
List<int> items = sample.MyList.GetListOfIntItems(count);
Assert.That(items.Count,Is.EqualTo(5));
}

قصد داريم بررسي كنيم آيا متد GetListOfIntItems واقعا همان تعداد آيتمي را كه بايد برگرداند، بازگشت مي‌دهد؟ عدد 5 به آن پاس شده است و در ادامه قصد داريم بررسي كنيم، count شيء حاصل (items در اينجا) آيا واقعا مساوي عدد 5 است؟
اگر آن را (سطر مربوط به Assert را) كلمه به كلمه بخواهيم به فارسي ترجمه كنيم به صورت زير خواهد بود:
مي‌خواهيم اثبات كنيم كه count مربوط به شيء items مساوي 5 است.

پس از اضافه كردن متد فوق، پروژه را كامپايل نمائيد.

اكنون برنامه nunit.exe را اجرا كنيد تا NUnit IDE ظاهر شود (در همان دايركتوري bin مسير نصب NUnit قرار دارد).
از منوي File آن يك پروژه جديد را آغاز نموده و آنرا ذخيره كنيد.
سپس از منوي project آن، با استفاده از گزينه add assembly ، فايل dll كتابخانه تست خود را اضافه نمائيد.
احتمالا پس از انجام اين عمليات بلافاصله با خطاي زير مواجه خواهيد شد:

---------------------------
Assembly Not Loaded
---------------------------
System.ApplicationException : Unable to load TestLibrary because it is not located under
the AppBase
----> System.IO.FileNotFoundException : Could not load file or assembly
'TestLibrary' or one of its dependencies. The system cannot find the file specified.
For further information, use the Exception Details menu item.

اين خطا به اين معنا است كه پروژه جديد NUnit بايد دقيقا در همان پوشه خروجي پروژه، جايي كه فايل dll كتابخانه تست ما توليد شده است، ذخيره گردد. پس از افزودن اسمبلي، نماي برنامه NUnit بايد به شكل زير باشد:



همانطور كه ملاحظه مي‌كنيد، NUnit با استفاده از قابليت‌هاي reflection در دات نت، اسمبلي را بارگذاري مي‌كند و تمامي كلاس‌هايي كه داراي ويژگي TestFixture باشند در آن ليست خواهد شد.
اكنون بر روي دكمه run كليك كنيد تا اولين آزمايش ما انجام شود. (شكل زير)



رنگ سبز در اينجا به معناي با موفقيت انجام شدن آزمايش است.

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

۱۳۸۷/۱۰/۰۸

آشنايي با آزمايش واحد (unit testing) در دات نت، قسمت 2


دلايل شانه خالي كردن از آزمايش واحد!

1- نوشتن آزمايشات زمان زيادي را به خود اختصاص خواهند داد.

مهمترين دليلي كه برنامه‌نويس‌ها به سبب آن از نوشتن آزمايشات واحد امتناع مي‌كنند، همين موضوع است. اكثر افراد به آزمايش به‌عنوان مرحله آخر توسعه فكر مي‌كنند. اگر اين چنين است، بله! نوشتن آزمايش‌هاي واحد واقعا سخت و زمانگير خواهند بود. به همين جهت براي جلوگيري از اين مساله روش pay-as-you-go مطرح شده است (ماخذ: كتاب Pragmatic Unit Testing در سي شارپ). يعني با اضافه شدن هر واحد كوچكي به سيستم، آزمايش واحد آن‌را نيز تهيه كنيد. به اين صورت در طول توسعه سيستم با باگ‌هاي كمتري نيز برخورد خواهيد داشت چون اجزاي آن‌را در اين حين به تفصيل مورد بررسي قرار داده‌ايد. اثر اين روش را در شكل زير مي‌توانيد ملاحظه نمائيد (تصويري از همان كتاب ذكر شده)




نوشتن آزمايشات واحد زمانبر هستند اما توسعه پيوسته آن‌ها با به تاخير انداختن آزمايشات به انتهاي پروژه، همانند تصوير فوق تاثير بسيار قابل توجهي در بهره وري شما خواهند داشت.

بنابراين اگر عنوان مي‌كنيد كه وقت نداريد آزمايش واحد بنويسيد، به چند سؤال زير پاسخ دهيد:
الف) چه مقدار زمان را صرف ديباگ كردن كدهاي خود يا ديگران مي‌كنيد؟
ب) چه ميزان زمان را صرف بازنويسي كدي كرده‌ايد كه تصور مي‌رفت درست كار مي‌كند اما اكنون بسيار مشكل زا ظاهر شده است؟
ج) چه مقدار زمان را صرف اين كرده‌ايد كه منشاء باگ گزارش شده در برنامه را كشف كنيد؟

براي افرادي كه آزمايشات واحد را در حين پروسه توسعه در نظر نمي‌گيرند، اين مقادير بالا است و با ازدياد تعداد خطوط سورس كدها، اين ارقام سير صعودي خواهند داشت.



تصويري از كتاب xUnit Test Patterns ، كه بيانگر كاهش زمان و هزينه كد نويسي در طول زمان با رعايت اصول آزمايشات واحد است

2- اجراي آزمايشات واحد زمان زيادي را تلف مي‌كند.

نبايد اينطور باشد. عموما اجراي هزاران آزمايش واحد، بايد در كسري از ثانيه صورت گيرد. (براي اطلاعات بيشتر به قسمت حد و مرز يك آزمايش واحد در قسمت قبل مراجعه نمائيد)

3- امكان تهيه آزمايشات واحد براي كدهاي قديمي ( legacy code ) من وجود ندارد

براي بسياري از برنامه نويس‌ها، تهيه آزمايش واحد براي كدهاي قديمي بسيار مشكل است زيرا شكستن آن‌ها به واحدهاي كوچكتر قابل آزمايش بسيار خطرناك و پرهزينه است و ممكن است سبب از كار افتادن سيستم آن‌ها گردد. اينجا مشكل از آزمايش واحد نيست. مشكل از ضعف برنامه نويسي آن سيستم است. روش refactoring ، طراحي مجدد و نوشتن آزمايشات واحد، به تدريج سبب طراحي بهتر برنامه از ديدگاه‌هاي شيءگرايي شده و نگهداري سيستم را در طولاني مدت ساده‌تر مي‌سازد. آزمايشات واحد اين نوع سيستم‌ها را از حالت فلج بودن خارج مي‌سازد.

4- كار من نيست كه كدهاي نوشته شده را آزمايش كنم!

بايد درنظر داشته باشيد كه اين هم كار شما نيست كه انبوهي از كدهاي مشكل دار را به واحد بررسي كننده آن تحويل دهيد! همچنين اگر تيم آزمايشات و كنترل كيفيت به اين نتيجه برسد كه عموما از كدهاي شما كمتر مي‌توان باگ گرفت، اين امر سبب معروفيت و تضمين شغلي شما خواهد شد.
همچنين اين كار شما است كه تضمين كنيد واحد تهيه شده مقصود مورد نظر را ارائه مي‌دهد و اين‌كار را با ارائه يك يا چندين آزمايش واحد مي‌توان اثبات كرد.

5- تنها قسمتي از سيستم به من واگذار شده است و من دقيقا نمي‌دانم كه رفتار كلي آن چيست. بنابراين آن را نمي‌توانم آزمايش كنم!

اگر واقعا نمي‌دانيد كه اين كد قرار است چه كاري را انجام دهيد به طور قطع الان زمان مناسبي براي كد نويسي آن نيست!

6- كد من كامپايل مي‌شود!

بايد دقت داشت كه كامپايلر فقط syntax كدهاي شما را بررسي كرده و خطاهاي آن‌را گوشزد مي‌كند و نه نحوه‌ي عملكرد آن‌را.

7- من براي نوشتن آزمايشات حقوق نمي‌گيرم!

بايد اذعان داشت كه به شما جهت صرف تمام وقت يك روز خود براي ديباگ كردن يك خطا هم حقوق نمي‌دهند! شما براي تهيه يك كد قابل قبول و قابل اجرا حقوق مي‌گيريد و آزمايش واحد نيز ابزاري است جهت نيل به اين مقصود (همانند يك IDE و يا يك كامپايلر).

8- احساس گناه خواهم كرد اگر تيم فني كنترل كيفيت و آزمايشات را از كار بي كار كنم!!

نگران نباشيد، اين اتفاق نخواهد افتاد! بحث ما در اينجا آزمايش كوچكترين اجزا و واحدهاي يك سيستم است. موارد ديگري مانند functional testing, acceptance testing, performance & environmental testing, validation & verification, formal analysis توسط تيم‌هاي كنترل كيفيت و آزمايشات هنوز بايد بررسي شوند.

9- شركت من اجازه اجراي آزمايشات واحد را بر روي سيستم‌هاي در حال اجرا نمي‌دهد.

قرار هم نيست بدهد! چون ديگر نام آن آزمايش واحد نخواهد بود. اين آزمايشات بايد بر روي سيستم شما و توسط ابزار و امكانات شما صورت گيرد.


پ.ن.
در هشتمين دليل ذكر شده، از acceptance testing نامبرده شده. تفاوت آن با unit testing به صورت زير است:

آزمايش واحد:
توسط برنامه نويس‌ها تعريف مي‌شود
سبب اطمينان خاطر برنامه نويس‌ها خواهد شد
واحدهاي كوچك سيستم را مورد بررسي قرار مي‌دهد
يك آزمايش سطح پائين ( low level ) به شمار مي‌رود
بسيار سريع اجرا مي‌شود
به صورت خودكار (100 درصد خودكار است) و با برنامه نويسي قابل كنترل است

اما در مقابل آزمايش پذيرش به صورت زير است:
توسط مصرف كنندگان تعريف مي‌شود
سبب اطمينان خاطر مصرف كنندگان مي‌شود.
كل برنامه مورد آزمايش قرار مي‌گيرد
يك آزمايش سطح بالا ( high level ) به شمار مي‌رود
ممكن است طولاني باشد
عموما به صورت دستي يا توسط يك سري اسكريپت اجرا مي‌شود
مثال : گزارش ماهيانه بايد جمع صحيحي از تمام صفحات را در آخرين برگه گزارش به همراه داشته باشد


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

۱۳۸۷/۱۰/۰۷

آشنايي با آزمايش واحد (unit testing) در دات نت، قسمت 1


آزمايش واحد چيست؟

آزمايش واحد (unit testing) هنر و تمرين بررسي صحت عملكرد قطعه‌اي از كد (كه در اينجا واحد ناميده شده است)، به وسيله كدهاي ديگري است كه توسط برنامه نويس نوشته خواهند شد. عموما اين آزمايش‌ها جهت بررسي يك متد تهيه مي‌شوند. در اين مرحله بايد درنظر داشت كه هدف، بررسي كارآيي نرم افزار نيست. هدف اين است كه بررسي كنيم آيا قطعه كد جديدي كه به برنامه اضافه شده است درست كار مي‌كند و آيا هدف اصلي از توسعه آن‌را برآورده مي‌سازد؟
براي مثال متدي را توسعه داده‌ايد كه آدرس يك دومين را از آدرس اينترنتي دريافت شده، جدا مي‌سازد. با استفاده از آزمايشات واحد متعدد مي‌توان از صحت عملكرد آن اطمينان حاصل كرد.


اهميت و مزاياي آزمايش واحد كدامند؟

  • كامپايل شدن كد به معناي صحت عملكرد آن نيست. حتما نياز به روش‌هايي براي آزمايش سيستم وجود دارد. صرفا به شما حقوق داده نمي‌شود كه كد بنويسيد. به شما حقوق داده مي‌شود كه كد قابل اجرايي را تهيه كنيد.
  • نوشتن آزمايش‌هاي واحد به توليد كدهايي با كيفيت بالا در دراز مدت منجر خواهد شد. براي نمونه فرض كنيد سيستمي را توسعه داده‌ايد. امروز كارفرما از شما خواسته است كه قابليت جديدي را به برنامه اضافه كنيد. براي اعمال اين تغييرات براي مثال نياز است تا قسمتي از كدهاي موجود تغيير كند، همچنين كلاس‌ها و متدهاي جديدي نيز به برنامه افزوده گردند. پس از انجام درخواست رسيده، چگونه مي‌توانيد اطمينان حاصل كنيد كه قسمت‌هاي پيشين سيستم كه تا همين چند لحظه پيش كار مي‌كردند، اكنون نيز همانند قبل كار مي‌كنند؟ حجم كدهاي نوشته شده بالا است. آزمايش دستي تك تك موارد شايد ديگر از لحاظ زماني مقدور نباشد. آزمايش واحد روشي است براي اطمينان حاصل كردن از اينكه هنگام تحويل كار به كارفرما مرتبا سرخ و سفيد نشويم! به اين صورت عمليات refactoring كدهاي موجود بدون ترس و لرز انجام خواهد شد، چون بلافاصله مي‌توانيم آزمايشات قبلي را اجرا كرده و از صحت عملكرد سيستم اطمينان حاصل نمائيم. بدون اينكه در زمان تحويل برنامه در هنگام بروز خطا بگوئيم : "اين غيرممكنه!"
  • روال‌هاي آزمايشات صورت گرفته در آينده تبديل به مرجع مهمي جهت درك چگونگي عملكرد قسمت‌هاي مختلف سيستم خواهند شد. چگونه فراخواني شده‌اند، چگونه بايد به آن‌ها مقداري را ارجاع داد و امثال آن.
  • با استفاده از آزمايش‌هاي واحد، بدترين حالات ممكن را قبل از وقوع مي‌توان در نظر گرفت و بررسي كرد.
  • نوشتن آزمايش‌هاي واحد در حين كار، برنامه نويس را وادار مي‌كند كه كار خود را به واحدهاي كوچكتري كه قابليت بررسي مستقلي دارند، بشكند. براي مثال فرض كنيد متدي را توسعه داده‌ايد كه پس از انجام سه عمليات مختلف بر روي يك رشته، خروجي خاصي را ارائه مي‌دهد. هنگام آزمايش اين متد چگونه مي‌توان اطمينان حاصل كرد كه كدام قسمت سبب شكست آزمايش شده است؟ به همين جهت برنامه نويس جهت ساده‌تر كردن آزمايشات، مجبور خواهد شد كه كد خود را به قسمت‌هاي مستقل كوچكتري تقسيم كند.
  • با توجه به امكان اجراي خودكار اين آزمايشات، به عنوان جزئي ايده‌آل از پروسه توليد نرم افزار محسوب مي‌شوند.


حد و مرز يك آزمايش واحد كجاست؟

آزمايش شما، آزمايش واحد ناميده نخواهد شد اگر:
  • با ديتابيس سر و كار داشته باشد.
  • با شبكه در ارتباط باشد.
  • با فايل‌ها كار كند.
  • نياز به تمهيدات ويژه‌اي براي اجراي آن وجود داشته باشد. مثلا وجود يك فايل config براي اجراي آن ضروري باشد.
  • همراه و همزمان با ساير كدهاي آزمايش‌هاي واحد شما قابل اجرا نباشد.
براي مثال اگر يكي از متدهاي شما بزرگترين عدد يك ليست را از ديتابيس دريافت مي‌كند، در متدي كه براي آزمايش واحد آن تهيه خواهيد كرد نبايد هيچگونه كدي جهت برقراري ارتباط با ديتابيس نوشته شود.
اين امر سبب سريع‌تر اجرا شدن آزمايشات واحد خواهند شد و در آينده شما را از انجام آن به‌دليل كند بودن روند انجام آزمايشات، منصرف نخواهد كرد. همچنين تغييرات انجام شده در لايه دسترسي به داده‌ها سبب غيرمعتبر شدن اين نوع آزمايشات نخواهند شد. به بيان ديگر وظيفه متد آزمايش واحد، اتصال به ديتابيس يا شبكه و يا خواندن اطلاعات از يك فايل نيست.

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