تست برنامه‌های مبتنی بر شیءگرایی

در مقاله‌ی قبل،‌ دو تا از مهمترین و جالبترین استراتژی‌های تست نرم‌افزار یعنی استراتژی‌های تست جعبه سفید و تست جعبه سیاه را معرفی کردیم. همانطور که گفتیم؛ این استراتژی‌ها معمولا ماژول‌های نرم‌افزاری را به‌صورت کلی بررسی می‌کنند. اما بعضی مواقع،‌ موقعیت‌هایی پیش می‌آید که در آنها می‌خواهیم مطمئن شویم که در یک کلاس،‌ روند اجرای برنامه حتما یک ترتیب خاصی را رعایت می‌کند و هیچ‌وقت این ترتیب به‌هم نمی‌ریزد. این موقعیت‌ها،‌ معمولا در برنامه‌های مبتنی بر شی‌ءگرایی ایجاد می‌شوند و در این مقاله می‌خواهیم درباره‌ی استراتژی تست نرم‌افزار در این موقعیت‌ها، صحبت کنیم. در ادامه با من همراه باشید.وی‍ژگی خاص برنامه‌های شیءگرا که عملیات تست را پیچیده می کندویژگی بارز برنامه‌های مبتنی بر شی‌ءگرایی این است که این برنامه‌ها معمولا از کلاس‌ها و ماژول‌های نرم‌افزاری مختلفی در کنارهم استفاده می‌کنند؛ در نتیجه در هنگام اجرای جریان کنترل برنامه،‌ مجبورند بارها و بارها بین خط‌های مختلف این کلاسها در رفت و آمد باشند و این موضوع باعث پیچیده شدن جریان اجرای برنامه می‌شود. از طرفی،‌در برنامه‌های شیءگرا، ما طبیعتا میتوانیم با ساختن آبجکت از یک کلاس،‌ از توابع درون کلاس استفاده کنیم. اما ممکن است منطق و مفهوم برنامه جوری چیده‌شده‌باشد که از نظر مفهومی اجازه نداشته باشیم تابعی را قبل از صدا زدن تابع دیگری استفاده کنیم. به هر حال ما به‌عنوان تست کننده‌ی این کلاسها،‌ باید مطمئن باشیم در این رفت‌وآمدها و پرش‌های کد،‌ ترتیب اجرای دستورات به درستی انجام می‌شود و جریان اجرای برنامه در اواسط راه گم نمی‌شود‍! از طرفی همانطور که گفتیم، در بعضی از کلاسها به دلیل الزاماتی که از نظر مفهومی وجود دارد،‌ ممکن است ما بخواهیم توابع و عملیات ها،‌ حتما طبق یک ترتیب خاص و مشخصی اجرا شوند. و از آنجاییکه در تست،‌ می‌خواهیم از برآورده شدن الزامات مطمئن شویم،‌ باید برای کنترل این موارد از استراتژی‌های تست کمک بگیریم.مفهوم دقیق یک Unit نرم‌افزاری در Unit Test چیست؟همانطور که در مقاله‌های قبل گفتیم؛ در Unit Test،‌ منظور ما از یک یونیت لزوما یک متد یا یک کلاس نیست! بلکه در هر بخش از برنامه،‌ مفهوم یونیت میتواند به طور مجزا تعریف شود. یکی از مفاهیمی که باعث متفاوت شدن مفهوم یک یونیت در برنامه‌نویسی شیءگرا شده‌است،‌ مفهوم encapsulation است. با استفاده از این مفهوم، در کنار کلاسی که عملیات‌ها را انجام می‌دهد،‌ یک کلاس دیگر نیز داریم که متغیرها را در خود نگه میدارد و به کمک توابع getter و setter،‌ می‌تواند به ما اجازه بدهد که مقدار متغیرها را بخوانیم و یا آن را تغییر دهیم. در این مقاله درباره‌ی اینکه چرا این کار را می‌کنیم و چطور آن را پیاده‌سازی می‌کنیم،‌ صحبت نمی‌کنیم چرا که هدف از این مقاله‌، بررسی نحوه‌ی تست‌کردن این کلاس‌ها است اما نکته‌ای که وجود دارد این است که با توجه به مطالب گفته‌شده، ما نمی‌توانیم این دو کلاس را به صورت مجزا تست‌کنیم چون بدون حضور یکدیگر عملکرد کاملی ندارند. در نتیجه مجموعه‌ی این کلاسها را یک یونیت در نظر می‌گیریم و آنها را در کنار هم مورد آزمایش قرار می‌دهیم.نمای کلی از Encapsulationدر واقع در Unit test، ما کلاسی که در حال اجرا است را به همراه Model ها و همچنین Controller ها یا Adapter های مربوطه که فرآیندهای مختلفی برای شروع به کار آن کلاس انجام میدهند را یک یونیت در نظر میگیریم تا بتوانیم رفتار کلی آن بخش را تست کنیم. البته این یونیت ها کوچکترین مفاهیمی که قابل تست کردن هستند نیستند و ما میتوانیم برای تک تک متد های درون برنامه تست بنویسیم. این کار زمانی بیشتر اهمیت پیدا می کند که بخواهیم متدهایی که در کلاس وجود دارند و از قسمتهای مختلف برنامه مورد استفاده قرار می گیرند را تست کنیم و یا در جریان روند برنامه و تحلیل رفتار آن، نیاز باشد که حتما یک روند خاصی در کنار الگوهای اختیاری دیگر وجود داشته باشد. برای مثال اگر بخواهیم کلاس مدیریت اکانت بانکی را بسازیم،‌ قاعدتا باید متدهایی مثل بازکردن حساب،‌بستن حساب،‌برداشت از حساب،‌ واریز به حساب و... داشته باشیم. با استفاده از این متدها و ترکیب‌های مختلف استفاده از آنها،‌ قاعدتا میتوانیم فرآیندهای مختلفی را طی کنیم. اما در هرحال، باید برای هرکاربر حتما ابتدا حساب بازکنیم و در نهایت هم باید دسترسی به حساب را ببندیم. و نمیتوانیم حتی ترتیب اینها را جابجا کنیم!‌ در این شرایط تست ها به کمک ما می‌آیند تا بتوانیم رفتار برنامه را شبیه سازی کنیم.در شکل زیر،‌ نمونه‌ای از testcase هایی که می‌توانیم برای کلاس مدیریت حساب‌های بانکی بنویسیم آورده شده‌است:چند نمونه از TestCaseهای کلاس مدیریت حساب‌های بانکیهمانطور که در خط اول دیده می‌شود،‌ از نظر عملکرد مفهومی یک حساب بانکی، حداقل عملیات‌هایی که باید "به‌ترتیب" اتفاق بیفتد،‌شامل بازکردن حساب، واردکردن مشخصات حساب،‌ سپرده‌گذاری، برداشت از حساب و در نهایت بستن حساب است. قطعا اگر قبل از اینکه حساب بازشود قصد داشته‌باشیم از آن برداشت کنیم،‌ مفهومی ندارد و قابل اجرا نیست! البته که عملیات‌های زیاد دیگری نیز ممکن است در بازه‌ی باز شدن حساب تا بستن آن پیش بیاید و محدودیتی در این باره وجود ندارد. تنها محدودیتی که هست این است که تمام عملیات‌های اساسی که در اولین فرآیند درون شکل آمده است،‌ حتما با ترتیب مشخص شده‌ اجرا شوند. TestCaseهای r1 و r2،‌ دو نمونه از فرآیندهای احتمالی دیگر را نشان می‌دهند.یک نقشه‌ی کلی از برنامه برای کمک به بهتر مشخص شدن TestCaseهاتست برنامه زمانی کامل می شود که تست نوشته شده،‌ تمامی حالاتی که ممکن است در روند برنامه نوشته شده رخ دهد را حداقل یکبار طی کرده باشد. کشیدن State Diagram یا دیاگرام حالت در این مسیر خیلی می‌تواند به ما کمک کند. به کمک این دیاگرام،‌ ‌حالت‌های مختلف اجرای نرم‌افزار را متوجه می شویم تا بتوانیم با دقت بیشتری برای آن تست طراحی کنیم. حال اگر کلاسی که میخواهیم بررسی کنیم، با کلاس های دیگری نیز همکاری می کند و درارتباط است، تعداد این دیاگرام های حالت بیشتر از یکی می شود و باید تمام آنها را در کنار هم بررسی کنیم تا جریان رفتاری نرم‌افزار را متوجه شویم و تمام حالات ممکن را در نظر بگیریم. در شکل زیر،‌ State Diagram مربوط به کلاس مدیریت حساب بانکی برای نمونه آورده‌شده است. با دنبال کردن جریان فرآیندها در این دیاگرام،‌ می‌توانیم تمام فرآیندهایی که این کلاس ممکن است طی کند را رصد کنیم.دیاگرام حالت مربوط به برنامه‌ی مدیریت حساب‌های بانکیSource : Roger S. Pressman, Bruce R. Maxim. "Software Engineering A Practtitioner's Approach" - 9th Editionدر مقاله‌ی بعدی، درباره‌ی تست یکپارچگی یا Integration Testing صحبت می‌کنیم. با توجه به اینکه Integration Test مربوط به یکپارچگی تک‌تک اعضای سیستم در کنار هم و تعامل آنها با یکدیگر می‌باشد، شامل مباحث جذابی می‌شود. از همین‌جا از شما دعوت می‌کنم مقاله‌ی بعدی را نیز مطالعه کنید.امیدوارم مقالات برای‌شما مفید بوده باشد. ممنون از همراهی شما