خلاصه: ما امیدواریم که بتوانیم در هنگام تست، تمام وضعیتها را در نظر بگیریم، اما از موارد غیرمعمول چشمپوشی کنیم. همین موضوع مزیت Random Test Generatorهاست. طبعا پس از تست کردن چندین Test Case، وقتیکه این ابزار صدها مورد را تولید میکنند، باید احساس بهتری داشته باشیم. با توجه به موارد بیشتری که در Test Generator تولید میشوند، احتمال بیشتری وجود دارد که در میان آنها چیزی جالب توجه وجود داشته باشد.
چقدر در مورد Random Testing میدانیم؟
امروز میخواهیم مقداری در مورد FsCheck صحبت کنیم. FsCheck یک ابزار برای تست اپلیکیشنهای NET. به صورت اتوماتیک است. با استفاده از این ابزار، کدنویس یک Specification از برنامه را به شکل Propertyهایی از Functionها، Methodها، یا Objectهایی که باید برآورده شوند، آماده میکند. سپس FsCheck تست میکند که این Propertyها به صورت تصادفی در تعداد زیادی از موارد تولید شده قرار بگیرند.
پس از انجام اولین آزمایش با FsCheck و تست مبتنی بر ویژگی(Property-Based Testing)، بسیار ناراحت شدم.
چون زبان برنامهنویسی Haskell در دسترس من بود و من تا به حال از آن استفاده نکرده بودم. چیزی که توجه من را به خود جلب کرد، ابزاری به نام QuickCheck و پارادایمی بود که آن معرفی کرد. از آنجا که من در فضای Net. کار میکردم، تصمیم گرفتم روی FsCheck(یک F# Port از QuickCheck) تحقیق کنم. نومیدی من ناشی از مجموعه ویژگیهای لیست شده بود.
به عنوان نمونه وقتی که یک لیست را معکوس میکنیم و سپس دوباره آن را معکوس کنیم، انتظار داریم لیست اولیه ارائه شود، درست است؟ این ویژگی باید بدون توجه به این باشد که لیست مذبور شامل چه مواردیست. تعجب من این است که تست دوم با FsCheck با شکست مواجه شد! اولین تست با لیستی از اعداد صحیح با موفقیت انجام شد، اما زمانی که من نوع داده را تغییر دادم و تست را با نوع داده اعشاری تکرار کردم، تست با شکست مواجه شد.
در اینجا FsCheck مقادیری تصادفی، برای پر کردن لیست ایجاد نموده و سپس بررسی میکند تا ببیند آیا Proprty مورد تست قرار گرفته است یا خیر. چندین بار ترکیبات مختلفی از مقادیر که نتوانستند با موفقیت انجام شوند، مورد بررسی قرار میگیرند. در رابطه با موارد Integer، فقط اعداد ۱، ۲، ۳ مدنظر نیست. بلکه بزرگترین و کوچکترین عدد Integer هم نیاز به بررسی دارند. علاوه بر این باید لیستهای خالی را نیز در نظر بگیریم. FsCheck همه این موارد را مورد بررسی قرار میدهد. حال سوالی که پیش میآید این است که چرا وقتی اعداد اعشاری را وارد کار کردیم، با شکست مواجه شدیم؟
براساس تعریف اعداد اعشاری، آنها الگوی بیت را دریافت کرده و سپس الگو را با فرض وجود اعشار تفسیر میکنند. با این وجود، همه ترکیبات ممکن روی بیتها به این ترتیب قابل تفسیر نیستند. ما این الگوهای مشکل را به اصطلاح (NaN(Not a Number مینامیم. در واقع متغیرهای NaN میتوانند بیت به بیت یکسان باشند، اما برابر نیستند. چنین چیزی معقول به نظر میرسد، اما این مورد غیر قابل انعطاف بوده و به آسانی نادیده گرفته میشود.
ما امیدواریم که بتوانیم در هنگام تست، تمام وضعیتهای آسیبپذیری را در نظر بگیریم، اما بسیار مرسوم است که از موارد غیرمعمول، چشمپوشی کنیم. این از مزیتهای Random Test Generatorها مانند FsCheck است. طبعا پس از تست کردن چندین Test Case، وقتیکه این ابزار صدها مورد را تولید میکنند، باید احساس بهتری داشته باشیم. با توجه به موارد بیشتری که در Test Generator تولید میشوند، احتمال بیشتری وجود دارد که در میان آنها چیزی جالب توجه وجود داشته باشد.
پس از اینکه FsCheck اولین Failure را یافت، نوبت به مرحله دوم میرسد. این ابزار تلاش میکند، مقدار دادههای مورد نیاز برای ایجاد مشکل را کاهش دهد. من در مرحله دوم متمرکز نخواهم شد، زیرا نکته من این است که دیدن اشتباهاتی مانند این که من فقط با NaN توضیح دادم، به ما در عمیقتر فکر کردن در رابطه با اینکه CodeBase چه کاری انجام میدهد کمک میکند(به ویژه پس از کاهش تستهای آن به کوچکترین شکل).
در مورد یک لیست از مقادیر اعشاری، ممکن است ما یک NaN Buinsess را رد کنیم. حتی ممکن است این کار را انجام دهیم. اما نکته اصلی در اجرای این تستها این است که ما راجع به کدنویسی فکر کنیم. شاید ما باید چیزی در لبههای(Edge) سیستم اضافه کنیم تا مقادیر NaN را خارج از آن نگاه دارد، و یا اینکه نیاز به این باشد که NaN نمیتواند از هیچگونه تغییری در آن بوجود آید.
کد ما دارای باگ است، چرا که تفکر ما در مورد آن دارای نقاط کور است. حتی بزرگترین Automatic Test Case Generatorها در جهان نیز دارای نقاط کور هستند. خوشبختانه دستگاه و انسان دارای نقاط کور متفاوت هستند؛ آنچنانکه هر یک میتواند نقطه کور دیگری را پوشش دهد.
ابزارهایی مانند FsCheck یا QuickCheck از پارادایم متفاوتی برای تست استفاده میکنند. به این صورت که از نقاط قوت دستگاه به منظور تکمیل نقاط ضعف ما استفاده میکنند. ماشینها میتوانند از مجموعهای گسترده از Test Dataها که هزینهبر و خستهکننده هستند استفاده کنند، اما آنها برای مصرف تمام دادهها نیاز به تستهای مبتنی بر Property دارند.
وقتی که به عنوان یک برنامهنویس مبتدی کار میکردم(که البته هنوز هم هستم)، این موضوع را درک نیمکردم. در آن زما روی پروژه آماری کار میکردم که به شدت به آمار و احتمالات بیزیوار(Bayesian) تکیه داشت. برخی چیزها وجود دارند که شما همیشه میتوانید با احتمالات شرطی بر آنها تکیه کنید. محدوده احتمالات برای هر چیزی، بین صفر تا یک است. به طور کلی، پس از جمع کردن تمامی احتمالات باید حاصلجمع با یک برابر باشد. این ویژگی را میتوان به صورت اتوماتیک بررسی کرد.
در این راستا اخیرا صحبتی با دوستان در مورد صدور صورتحساب پیش آمد و البته سوالاتی نیز پیرو آن مطرح شد، مانند اینکه آیا تمام آیتمهای خطی مثبت هستند؟ میتوانیم بدانیم که آیا یک زیرمجموعه متشکل از تعداد زیادی آیتم خطی، میتواند از زیرمجموعهای کمتر از آیتمهای خطی تجاوز کند؟ آیا می توان مجموعهها را بدون توجه به ترتیبی که آنها در آن جمع شدهاند تغییر داد؟ من پرسیدم که چه Propertyهای دیگری باقی میماند که بتوانیم در مقابل آنها دفاع کنیم. هر یک از این تغییرات میتوانند به عنوان مبنایی برای تستهای مبتنی بر Property ایفای نقش نمایند. شما حتی میتوانید بر سر ارسال قیمتهای منفی در این سیستم شرطبندی کنید.
طیف گستردهای از تستهای مبتنی بر Property، میتوانند مقدار زیادی از آموزش را درباره CodeBaseمان به ما ارائه دهند. فرآیند تست هرگز ثابت نمیکند که یک CodeBase بدون خطا است، بلکه با تست، میتوان ادعا کرد که تعداد خطاهای پنهان کاهش مییابد. ما برای یادگیری بیشتر، باید درک کنیم که چه تستهایی را باید انجام دهیم و البته به این موضوع هم دقت داشته باشیم که تستها چه چیزهایی را نشان میدهند. دقیقا مانند FsCheck که نرخ شکستهای سنگین را با اشکال سادهتری به نسبت دیگر ابزارها و تکنیکها کاهش میدهد. همچنین همیشه در نظر داشته باشیم که باید تستها را به صورت دورهای تکرار کنیم.
آیا هر تست به ما چیزی بیش از گزارشات مربوط به نرخ باگها میدهد؟ هدف ما نباید فهمیدن نرخ باگها باشد بلکه به حداکثر رساندن میزان درک از موفقیت یا شکست هر تست است که برای ما ارزشمند مینماید.
وقتی که تست مبتنی بر Property روی فیلدهای اعشاری بوسیله NaN شکست خورد و Fail شد من وقاقعا شگفت زده شدم. این به دلیل عملکرد بد روی لیست معکوس نبود؛ بلکه به دلیل فرضیه ذاتی درون تست در مورد چگونگی مقایسه اعداد اعشاری حادث شد. چنین رخدادی به ما یادآوری میکند که باید دادههایی را جستجو کنیم که با فرضیات ما مخالف هستند. هر چه این شگفتیها بیشتر شوند، باعث میشود در آخر سیستمهای ما قدرت بیشتری کسب کنند. این امر مستلزم دیدن چند پارادایم متنوع و توانایی برای استفاده از نقاط قوت هر یک است.