Robolectric با ایجاد یک محیط Runtime که شامل کد واقعی Android Framework است، کار میکند. این بدین معنیست که وقتی تستهای شما یا کد تحت تست در Android Framework فراخوانی میشود، یک تجربه واقعگرایانه بدست میآورید، چرا که در اکثر موارد همان کد اجرا میشود؛ همانطور که در یک دستگاه واقعی چنین اتفاقی میافتد. با این وجود محدودیتهایی هم وجود دارد:
- کد بومی(Native Code): کد بومی Android نمیتواند بر روی ماشین توسعه(Development Machine) شما اجرا شود.
- فراخوانی خارج از فرآیند: سرویسهای سیستمی در حال اجرای Android روی ماشین توسعه وجود ندارند.
- تست ناکافی APIها: Android در کنار هیچ API مناسبی برای تست قرار نمیگیرد.
Robolectric این شکافها و محدودیتها را با مجموعهای از کلاسها که به نام Shadows شناخته میشوند، پر میکند. هر Shadow میتواند رفتار یک کلاس متناظر را در سیستم عامل آندروید را تغییر یا بسط و گسترش دهد. هنگامی که یک کلاس Android ایجاد میشود، Robolectric به دنبال یک کلاس Shadow متناظر خواهد بود، و اگر آن را پیدا کند، یک Shadow Object برای ارتباط با آن ایجاد میکند.
Robolectric با استفاده از ابزار کدگذاری بایت(Byte Code Instrumentation) میتواند در پیادهسازی Fake Cross Platform تنیده شود، تا بدین ترتیب جانشین کد بومی شده و APIهای اضافی دیگری را وارد کار نماید تا با این ترفند تست ممکن شود.
در یک Name چیست؟
چرا “Shadow”؟ Shadow Objectها نه کاملا Proxy هستند، نه کاملا Fake هستند، نه کاملا Mock یا Stub هستند. Shadowها(سایهها) گاهی پنهان بوده و گاهی دیده میشوند و میتوانند شما را به سمت Object واقعی هدایت کنند.
Shadow Classها
Shadow Classها همیشه به Constructor بدون آرگومان نیاز دارند تا Robolectric Framework بتواند آنها را بسازد. آنها با کلاسهایی که توسط حاشیهنویسی ‘Implements@’ در اعلام کلاس، Shadow میکنند، مرتبط هستند.
Shadow Classها باید سلسله مراتب ارثبری کلاسهای Production را تقلید کنند. برای مثال، اگر شما در حال Impelment کردن یک Shadow برای ‘ViewGroup’ با عنوان ‘ShadowViewGroup’ هستید، آنگاه Shadow Class باید سوپرکلاس Sahdow برای ‘ViewGroup’ یعنی ‘ShadowView’ را Extend نماید.
... @Implements(ViewGroup.class) public class ShadowViewGroup extends ShadowView { ...
متدها
Shadow Objectها، متدهایی را Implement میکنند که به عنوان Android Class دارای Signature یکسان هستند. Robolectric وقتی که یک متد با Signature یکسان بر روی Android Object فراخوانی میشود، متد را روی یک Shadow Object فراخوانی میکند.
فرض کنید یک اپلیکیشن، Line Of Code زیر را تعریف کرده است:
... this.imageView.setImageResource(R.drawable.pivotallabs_logo); ...
تحت شرایط تست، متد زیر روی Shadow Instance فراخوانی خواهد شد:
‘ShadowImageView#setImageResource(int resId)’
متدهای Shadow باید با حاشیهنویسی به صورت ‘Implementation@’ مشخص شوند. Robolectric برای کمک به حصول اطمینان در انجام صحیح این روش یک lint test را در خود گنجانده است.
@Implements(ImageView.class) public class ShadowImageView extends ShadowView { ... @Implementation protected void setImageResource(int resId) { // implementation here. } }
Robolectric برای تمام متدهای روی Original Class که شامل ‘private’، ‘static’، ‘final’ یا ‘native’ باشند، از Shadow کردن پشتیبانی میکند.
همچنین معمولا، متدهای ‘Implementation@’ باید modifier(اصلاحکننده) از نوع ‘protected’ داشته باشند. در اینجا هدف این است که API Surface Area را برای Shadowها کاهش دهیم؛ نویسنده تست(Test Author) همیشه باید چنین متدهایی را به طور مستقیم در Android Framework Class فراخوانی کند.
مهم است که متدهای Shadow بر روی Shadow متناظر با کلاس که در ابتدای آنها تعریف شده بود، پیادهسازی شوند. در غیر این صورت مکانیزم جستجو Robolectric آنها را پیدا نخواهد کرد(حتی اگر آنها در SubClass-زیرکلاس Shadow اعلام شده باشند). به عنوان مثال، متد ‘()setEnabled’ روی View تعریف شده است. اگر متد ‘()setEnabled’ در ShadowViewGroup به جای ‘ShadowView’ تعریف شده باشد، در زمان اجرا آنرا پیدا نخواهید کرد، حتی زمانی که ‘()setEnabled’ روی یک نمونه از ‘ViewGroup’ فراخوانی میشود.
Constructorها برای Shadow کردن
هنگامی که یک شی Shadow نمونهسازی میشود، Robolectric به دنبال یک متد به نام __constructor__ که با “Implementation@” حاشیه نویسی شده است خواهد گشت. این متد باید دارای همان آرگومانهایی باشد که Constructorای که روی یک Object واقعی فراخوانی میشود دارای آنهاست.
به عنوان مثال، اگر کدِ اپلیکیشن، TextView Constructor را فراخوانی کند و این متد یک Context را دریافت نماید کدی مانند زیر خواهیم داشت:
new TextView(context);
Robolectric متد ‘__constructor__’ پایین را که یک Context دریافت میکند را فراخوانی مینماید:
@Implements(TextView.class) public class ShadowTextView { ... @Implementation protected void __constructor__(Context context) { this.context = context; } ...
دسترسی به Instance واقعی
گاهی اوقات ممکن است Shadow Classها مایل باشند به Objectای Refer کنند که دارای Shadow هستند، به عنوان مثال برای دستکاری در Fieldها. یک Shadow Class میتواند این کار را با اعلام یک فیلد که با ‘RealObject@’ حاشیهنویسی شده است انجام دهد:
@Implements(Point.class) public class ShadowPoint { @RealObject private Point realPoint; ... public void __constructor__(int x, int y) { realPoint.x = x; realPoint.y = y; } }
Robolectric قبل از فراخوانی هر متد دیگری، realPoint را به نمونه واقعیِ ‘Point’ سِت میکند.
مهم است که توجه داشته باشیم، متدهایی که روی Object واقعی فراخوانی میشوند، همچنان توسط Robolectric متوقف شده و مجددا هدایت میشوند. این مسئله در کدِ تست(Test Code) اهمیت چندانی ندارد، اما برای پیادهسازی کلاسهای Shadow، پیامدهای مهمی دارد. از آنجایی که سلسله مراتب ارثبری Shadow Classها همیشه انعکاس Android Classهای مرتبط با آنها نیست، گاهی اوقات لازم است که از طریق این Objectهای واقعی فراخوانی صورت گیرد، تا Robolectric در زمان اجرا برای Rout کردن آنها به Shadow Class درست و صحیح(بر اساس کلاس واقعیِ Object) فرصتی بدست آورد.
متدهای روی Shadow Class شما با استفاده از ‘()Shadow.directlyOn’ قادر به فراخوانی Android OS Code هستند.
تمام قسمتهای آموزش Robolectric، به صورت دستهبندی شده از اینجا نیز در دسترس است.