تست کدها در اندروید – بخش دوم

نویسنده : سید ایوب کوکبی ۱۲ شهریور ۱۳۹۷

تست کدها در اندروید - بخش دوم

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

آیا معماری شما تست‌پذیر است؟

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

اما این حرف، درست نیست

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

پییش به سوی یک معماری تست‌پذیر

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

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

پیروی نکردن از هیچ یک از الگوهای معماری

الگوهای معماری زیادی همچون MVP, MVVM, MVI و … وجود دارند که استفاده از آن‌ها نه‌تنها تست کردن کدها را آسان‌تر بلکه انعطاف‌پذیری و نگهداری کدها را هم سهولت می‌بخشد. این الگوها به شما این توانایی را می‌دهند تا با صرف کمترین زمان بتوانید ساختار فعلی کدهای خود را کاملاً درک کنید، فیچرهای جدید را به برنامه اضافه کنید و وقتی با مشکلی مواجه شدید به آسانی دیباگ و اشکال‌زدایی کنید. در این مقاله می‌توانید درباره‌ی الگوهای مختلف معماری در اندروید اطلاعات خوبی به دست آوردید.

عدم استفاده از تزریق وابستگی

اگر در کدبیس‌، تعداد کلید‌واژه‌ی new زیادی به چشم می‌خورد یعنی وقت زیادی باید صرف تست کردن آن کنید. اشتباه برداشت نکنید؛ منظور من این نیست که نباید هیچ شی جدیدی بسازید. قطعاً نمونه‌سازی کلاس‌ها و ساخت شی از آن‌ها نیازمند new است ولی حرف من این است که همه new کردن‌ها باید از یک نقطه مرکزی انجام شود و به صورت پراکنده در کد شاهد چنین کاری نباشیم. یعنی به گونه‌ای باشد که برای ساخت هر شی‌ای تنها و تنها از یک دروازه مرکزی اقدام شود. وقتی به جای تزریق از new کردن استفاده می‌کنید در واقع کار نوشتن تست را سخت‌تر می‌کنید. سعی کنید درباره‌ی تزریق وابستگی (Dependency Injection) تحقیق کنید و اینکه چطور از ابزارهای مفیدی مثل Dagger 2 برای سهولت کار استفاده نمایید.

بخوانید  چرا از جاوا به زبان کاتلین (Kotlin) سوئیچ کنیم؟

نقض قاعده SRP

اگر کلاسی بیشتر از وظیفه‌اش کار انجام دهد یعنی یک جای کار می‌لنگد، یعنی معماری کد با مشکل مواجه است. در گیت‌هاب بارها کدهایی را شاهد بوده‌ام که توسعه‌دهنده برای یک کلاس چندین و چند مسئولیت مختلف تعریف کرده است. این اشتباه است اصل تک مسئولیتی (SRP: Single Responsibility Principle) که یکی از مهم‌ترین اصول SOLID در مهندسی نرم‌افزار است این را می‌گوید که هر کلاس یا متدی باید تنها و تنها یک مسئولیت یا وظیفه بر عهده داشته باشد.

در بحث SRP مسئولیت یا Responsibility به «دلیلی برای تغییر» تعبیر می‌شود. اگر بیش از یک دلیل برای تغییر کلاس داشته باشید قاعده‌ی تک مسئولیتی نقض شده است.

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

سازنده‌های چاق

هدف اصلی از سازنده‌ی یک کلاس (Constructure) مقداردهی اولیه‌ی متغیرهای آن کلاس است و بس. اگر سازنده‌ی کلاس کاری به جز این انجام دهد تست‌پذیری آن کلاس را سخت کرده‌اید. استفاده از منطق شرطی (if, else, switch و …)، حلقه‌ها و فراخوانی متدهای استاتیک نشان از به هم ریخته بودن کد و تست دشوار آن دارد. سازنده را مختصر و تمییز نگه دارید نه مثل کد پایین که اینقدر چاق شده است:

public class Order {

private String id;
private String origin;
private double price;

public Order(String id, String origin, double price) {
this.id = id;
this.origin = origin;

if (price < 0) {
this.price = 0;
} else {
this.price = price;
}
}
}

 

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

استفاده زیاد از سینگلتون و استاتیک

اگر به دفعات زیادی از متدهای استاتیک در جای جای کدهای خود استفاده کرده‌اید تعجب نکنید که چرا تست کردن کد اینقدر سخت شده است. متدهای استاتیک را نمی‌توان به کمک کتابخانه‌ی Mocking استاندارد، جعل کرد. همچنین توسط زیرکلاس‌ها نیز قابل استفاده نیست. متدهای استاتیک گهگاهی می‌توانند مفید واقع شوند ولی هرچقدر بیشتر از آن‌ها استفاده کنید به همان میزان تست‌پذیری کد را هم پایین آورده‌اید. همین موضوع درمورد اشیاهء Singletons (که تضمین می‌کنند فقط یک نمونه از کلاس در طول اجرای برنامه وجود داشته باشد) نیز صادق است. این کلاس‌ها را نمی‌توان در تست‌ها ارجاع داد و در واقع مفهوم ایزوله بودن تست‌های واحد را هم زیرسوال می‌برد.

عدم استفاده از اینترفیس‌ها در کدنویسی

چیز دیگری که در کدها باعث پیچیدگی فرایند تست می‌شود بحث استفاده نکردن از اینترفیس‌ها (Coding Against Interfaces) است که باعث از دست دادن انعطاف‌پذیریِ تزریق mock یا پیاده‌سازی مصنوعی رفتار واقعی کلاس خواهد شد و این موضع تست کردن کلاس را بسیار دشوار می‌کند. ولی اگر از اینترفیس‌ها استفاده کنید، جایگزین کزدن پیاده‌سازی آن کلاس برای تست کار ساده‌تری است.

بخوانید  قسمت دوم :اکتیویتی ها

تخطی از قاعده‌ی LoD

قاعده‌ی (LoD: Law of Demeter) که تحت عنوان «اصل حداقل دانش» هم شناخته می‌شود بیان می‌کند که هر موجودیتی در کد باید حداقل دانش درباره‌ی دیگر قسمت‌ها داشته باشد. هرچقدر اطلاعات یک جز نسبت به سایر اجزاء کد بیشتر شود در واقع سطح وابستگی آن جزء بیشتر شده و این یعنی درهم‌تنیدگی کدها که دشواری تست را در پی خواهد داشت. شما باید هر یک از وابستگی‌ها (که در چارچوب تست آن کلاس نیست) را جعل کنید تا نهایتاً به تست کلاس اصلی برسید. چیزی شبیه پیدا کردن سوزن در انبار کاه. توصیه می‌کنم برای کسب اطلاعات بیشتر درباره‌ی LoD اینجا را بخوانید.

درک آزمون واحد

آزمون واحد (Unit Test) را می‌تواند بهترین نمونه مستقل از برنامه برای عملیات تست دانست. هدف اصلی‌اش اطمینان دادن از این موضوع است که بخش ایزوله‌ای از برنامه کارش را به درستی انجام می‌دهد. به آزمون واحد تنها از دید تضمین کیفی نگاه نکنید. این تست‌ها صحت عملکرد کد را نشان می‌دهند. در واقع مطمئن هستید که چیزهایی که الان کار می‌کند در آینده نیز همینطور خواهد بود. این تست‌ها برای فانکشن‌های خالص (pure function) بهترین عملکرد را به همراه خواهند داشت. طبق توضیحات ویکی‌پدیا، توابع خالص دو ویژگی دارند:

  1. مقدار برگشتی تنها به پارامترهای تابع وابسته بوده و از متغیرهای استاتیک، متغیرهای کلاس و … در آن استفاده نشده است؛
  2. باعث بروز اثرات جانبی (Side Effect) نمی‌شوند.

نوشتن آزمون‌های واحدِ خوب

نوشتن یک آزمون واحدِ خوب کار سختی نیست، اما چند موردی هست که موقع نوشتن این تست‌ها باید رعایت کنید:

سریع

این مهم‌ترین فاکتوری است که باید در ذهن داشته باشید. آزمون‌های واحد سریع اجرا می‌شوند. منظور من خیلی خیلی سریع است. منظورم این نیست که هر ثانیه یک Unit Test اجرا شود. بلکه در یک ثانیه باید چندین هزار تست انجام شود. هرچقدر سرعت این عملیات بیشتر باشد، دوست دارید بیشتر این تست‌ها را اجرا کنید. و هر چه قدر بیشتر این تست‌ها را اجرا کنید، ارزش بیشتری می‌توانید به دست آورید. معمولاً وقتی یونیت تست‌های شما کند اجرا می‌شوند، اجرای آن‌ها را به تعویق می‌اندازید که این موضوع در تضاد با هدف و فلسفه‌ی اصلی آزمون‌های واحد است.

کوچک و هدف‌مند

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

قابل اعتماد

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

بخوانید  تجربیاتی ارزشمند از یک سِنیور دولوپر اندروید

جامعیت

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

قبل از رفع یک باگ، تستی برای تولید آن باگ بنویسید. رابرت سی مارتین (Robert C. Martin)

آناتومی یک آزمون واحد

خب وقتش رسیده تا کمی درباره‌ی ساختار آزمون واحد صحبت کنیم. یک آزمون واحد در ساده‌ترین شکلش، ساختار (AAA: Arrange, Act and Assert) دارد. در ادامه هر یک از این قسمت‌ها را به صورت مجزا توضیح می‌دهم.

Arrange (مقداردهی و طبقه‌بندی داده‌های ورودی)

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

when(userRepository.isUsernameValid(username)).thenReturn(true);
 when(userRepository.isPasswordValid(password)).thenReturn(true);

اینجا ما از فریم‌ورک Mockito جهت موک کردن userRepository و برگرداندن پاسخ‌های از قبل آماده برای متدهای وابسته استفاده کرده‌ایم.

توجه: این قسمت کاملاً اختیاری است. ممکن است تستی نیازمند Arrange نباشد.

 Act (عمل)

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

boolean result = authenticator.login(username, password);

 

Assert (اثبات)

در این قسمت انتظارات شما از تست بررسی یا اثبات می‌شود. به عنوان مثال برای متد sum که دو عدد را جمع می‌کند، از ورودی ۲ و ۲ انتظار خروجی ۴ داریم. برای لاگین، Assertion ما می‌توان چنین چیزی باشد:

assertTrue(result);

 

این هم نمونه‌ی کاملی از یک آزمون واحد که سه قسمت بیان شده را نشان می‌دهد:

@Test
public void login_successIfUserNameAndPasswordIsValid() {
// Arrange
String username = "username";
String password = "password";

when(userRepository.isUsernameValid(username)).thenReturn(true);
when(userRepository.isPasswordValid(password)).thenReturn(true);

// Act
boolean result = authenticator.login(username, password);

// Assert
assertTrue(result);
}

 

معمولاً در اغلب کدهای متن‌باز این الگو رعایت می‌شود. پس بهتر است شما نیز همین الگو را برای آزمون‌های واحد دنبال کنید. الگوی دیگری هم هست که ابتدا بخش Act نوشته می‌شود، سپس Assertion و در آخر Arrangement. خب تا اینجا فعلاً کافی است. انشاء ا… در آینده مطالب بیشتری درباره‌ی تست مطرح می‌کنم. در صورتی که مقاله به دلتان نشست، ما را از دیدگاه‌های ارزشمند خود بهره‌مند سازید.

سید ایوب کوکبی

نویسنده و مترجم...

0 دیدگاه

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *