در دو قسمت قبلی این فصل یعنی آموزش اندروید-فصل ۲۹-۱: تست برنامههای اندروید و آموزش اندروید-فصل ۲۹-۲: تست برنامه اندروید با JUnit و Mockito درباره اصول اولیه تست واحد یا یونیت تست گفتم. در ادامه میخواهم وارد یکی از مهمترین بخشهای تست برنامههای اندروید بشوم: تست ابزاری برنامههای اندروید.
اسپرسو چیست؟
اسپرسو یک چارچوب تست برنامه است که در «کتابخانه پشتیبان تست اندروید» یا «Android Testing Support Library» قرار دارد. اسپرسو APIهایی برای شبیه سازی اعمال کاربر و نوشتن تستهای بررسی عملکرد UI در اختیار ما میگذارد. تستهای اسپرسو بر اساس کارهایی که کاربر میتواند یا ممکن است در زمان استفاده از برنامه انجام دهد نوشته میشود.
اگر خیلی ساده بخواهم بگویم در این تستها ما
- عناصر UI را در صفحه پیدا میکنیم و
- با این عناصر تعامل کرده یا وضعیت آنها را بررسی میکنیم
تستهای ابزاری اندروید در شاخه app/src/androidTest/java پروژه قرار دارند و ما هم تستهای خود را در همین مسیر خواهیم نوشت. تستهای اسپرسو از سه مؤلفه اصلی تشکیل شدهاند:
- ViewMatchers: اینها مجموعهای از اشیا هستند که از آنها برای پیدا کردن view ی مورد نظر در ساختار سلسله مراتبی صفحه استفاده میشود. آنها را به متد onView میفرستیم تا view مورد نظر را پیدا کنند و به ما برگردانند.
- ViewActions: از اینها برای انجام کارهایی مثل کلیک بر روی view ها استفاده میشود. آنها را به متد ()ViewInteraction.perform میفرستیم.
- ViewAssertions: از اینها برای اثبات و بررسی وضعیت ویو استفاده میکنیم. آنها را به متد ()ViewInteraction.check میفرستیم.
برای مثال:
onView(withId(R.id.my_view)) // withId(R.id.my_view) - ViewMatcher .perform(click()) // click() - ViewAction .check(matches(isDisplayed())); //matches(isDisplayed()) - ViewAssertion
تنظیم پروژه برای استفاده از اسپرسو
برای استفاده از اسپرسو در پروژه از اندروید SDK مخزن «Android Support Repository» را نصب کنید:
توصیه میشود که به تنظیمات شبیهساز اندروید خود بروید و انیمیشنهای دستگاه را غیر فعال کنید تا سرعت تستها بالاتر برود:
حالا به سراغ تنظیمات بیلد گریدل مربوط به ماژول برنامه بروید. اگر پروژه را با ویزارد اندروید استودیو ساخته باشید در این قسمت نیازی نیست هیچ کاری بکنید و اندروید استودیو به صورت خودکار هر آنچه که نیاز دارید را به این فایل اضافه کرده است. چک کنید ببینید در بخش defaultConfig خط زیر وجود داشته باشد:
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
و در قسمت وابستگیها یا dependencies خطهای زیر وجود داشته باشند:
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' })
برای این که ببینید آیا تنظیمات بیلد گریدل را به خوبی انجام دادهاید، به این نمونه دقت کنید:
apply plugin: 'com.android.application' android { compileSdkVersion 26 buildToolsVersion "26.0.1" defaultConfig { applicationId "ir.smartlab.spressodemo" minSdkVersion 15 targetSdkVersion 26 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' }) compile 'com.android.support:appcompat-v7:26.0.0-alpha1' compile 'com.android.support.constraint:constraint-layout:1.0.2' testCompile 'junit:junit:4.12' }
پروژه را سینک کنید! بعد از اتمام دانلود موارد جدید و سینک شدن پروژه، همه چیز آماده است تا یک فایل چیدمان یا layout درست کنیم و اولین تست ابزاری خودمان را بر روی آن اجرا کنیم.
اسپرسو چطور کار میکند؟
برای درک بهتر فرایند تست با اسپرسو، نگاهی از بالا به آن میاندازیم. ابتدا باید یکی از عناصر ویو را انتخاب کنیم. مثلا دکمهای که کاربر باید روی آن کلیک کند. سپس عملیاتی که کاربر باید روی آن ویو انجام دهد را شبیهسازی میکنیم. مثلا کاربر روی دکمه کلیک میکند یا متنی را در یک EditText وارد میکند. در انتها بررسی میکنیم تا ببینیم آیا رفتاری که انتظار داریم از برنامه رخ داده است یا نه؟ مثلا به صفحه بعد رفتهایم؟ کل فرایند تست ابزاری با اسپرسو همین است: پیدا کردن، انجام عملی که از کاربر انتظار داریم و بررسی رفتار برنامه در واکنش به آن عمل.
برای شروع کار با اسپرسو و نوشتن اولین تست ابزاری با اسپرسو، یک پروژه میسازیم. در صفحه اول این پروژه دو دکمه قرار دارد: دکمه Login که ما را به صفحه ورود کاربر میبرد و دکمه Sign Up که ما را به صفحه ثبت نام برنامه هدایت میکند. در تست اول دکمه Login را تست میکنیم. میخواهیم ببینیم که آیا کلیک بر روی این دکمه ما را به صفحه ورود برنامه میبرد یا نه. در تست دوم که در صفحه Login اتفاق میافتد، میخواهیم ببینیم پر کردن فرم لاگین و ارسال فرم لاگین به سرور آیا ما را به صفحه ورود موفق میبرد یا نه. بنابراین به اکتیویتیهای Login و LoginSuccess هم احتیاج خواهیم داشت.
حالا بیایید پروژه را بسازیم.
ساخت پروژه
برای شروع یک اکتیویتی به نام MainActivity بسازید و لیاوت آن را به شکل زیر طراحی کنید:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" tools:context="ir.smartlab.spressodemo.MainActivity"> <Button android:id="@+id/login_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Login" android:layout_marginLeft="8dp" /> <Button android:id="@+id/signup_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Sign Up"/> </LinearLayout>
این صفحه شبیه عکس زیر خواهد شد:
سپس اکتیویتی Login را به شکل زیر بسازید:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" app:popupTheme="@style/ThemeOverlay.AppCompat.Light" /> <LinearLayout android:id="@+id/layout_login" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginLeft="8dp" android:layout_marginRight="8dp" android:layout_marginTop="16dp" android:orientation="vertical"> <EditText android:id="@+id/edit_text_email" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="Email" android:inputType="textEmailAddress" /> <EditText android:id="@+id/edit_text_password" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="Password" android:inputType="textPassword" /> <Button android:id="@+id/button_login" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="Login" /> </LinearLayout> <ProgressBar android:id="@+id/progress_bar_login" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:layout_marginTop="8dp" android:indeterminate="true" android:visibility="gone" /> </RelativeLayout>
و در فایل کلاس این اکتیویتی تغییرات زیر را اعمال کنید تا کلیک بر روی دکمه Login صفحه LoginActivity را باز کند:
public class MainActivity extends AppCompatActivity { private Button loginButton; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); loginButton = (Button) findViewById(R.id.login_button); loginButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { startActivity(new Intent(MainActivity.this, LoginActivity.class)); } }); } }
یک بار پروژه را اجرا کنید و بر روی دکمه Login کلیک کنید تا از صحت عملکرد آن مطمئن شوید.
نوشتن اولین تست اسپرسو
حالا در نمای Project اندروید استودیو شاخه androidTest را باز کنید:
فایل ExampleInstrumentedTest برای نمونه است. میتوانید آن را پاک کنید. حالا یک پکیج یا بسته جدید در این مسیر ایجاد میکنیم به نام mainactivity و در آن یک کلاس ایجاد میکنیم به نام MainActivityTest:
ابتدا به این کلاس دو حاشیه نوشت یا annotation اضافه میکنیم:
@RunWith(AndroidJUnit4.class) @LargeTest public class MainActivityTest { // ... }
حاشیهنوشت @RunWith کلاسی که قرار است این تست را اجرا کند معرفی میکند. در حال حاضر مقدار AndroidJUnit4.class را به این حاشیهنوشت میدهیم. حاشیهنوشت دیگری که به این کلاس اضافه کردیم @LargeTest است. تستهایی را که احتمال میدهیم زمان اجرای آنها طولانی (بیشتر از یک ثانیه) است با این حاشیهنوشت مشخص میکنیم. این تستها معمولا تستهایی هستند که برای بررسی یکپارچگی کل برنامه از آنها استفاده میشود و ممکن است از تمام منابع سیستم مانند دیتابیس یا پایگاه داده و فایل و شبکه و … استفاده کنند. اگر تست بخش کمتر و کوچکتری را تست میکند میتوانید به جای @LargeTest از @MediumTest یا حتا @SmallTest استفاده کنید.
در گام بعدی یک قانون یا Rule به کلاس تست اضافه میکنیم:
@RunWith(AndroidJUnit4.class) @LargeTest public class MainActivityTest { @Rule public ActivityTestRule&lt;MainActivity&gt; mMainActivityTestRule = new ActivityTestRule&lt;MainActivity&gt;(MainActivity.class); // ... }
این حاشیهنوشت اعلام میکند که تستها بر روی اکتیویتی MainActivity اجرا میشوند و سیستم این اکتیویتی را قبل از اجرای تست و قبل از اجرای هر متد @Before اجرا میکند و بعد از اتمام تست و اجرای همه متدهای @After میبندد.
حالا زمان آن است که اولین تست را بنویسیم:
@RunWith(AndroidJUnit4.class) @LargeTest public class MainActivityTest { @Rule public ActivityTestRule&lt;MainActivity&gt; mMainActivityTestRule = new ActivityTestRule&lt;MainActivity&gt;(MainActivity.class); @Test public void clickLoginButton_openLoginScreen() { //locate and click on the login button onView(withId(R.id.button_login)).perform(click()); //check that the login screen is displayed onView(withId(R.id.edit_text_email)).check(matches(allOf(isDescendantOfA(withId(R.id.layout_login)), isDisplayed()))); } }
کل تست از دو خط تشکیل شده است. در خط اول به دنبال یک ویو با شناسه button_login میگردیم و روی آن کلیک میکنیم:
onView(withId(R.id.button_login)).perform(click());
در خط دوم چک میکنیم که آیا در میان فرزندان (isDescendantOfA) ویویی که حالا نمایش داده میشود (isDisplayed) و شناسه آن (layout_login) است، آیا ویویی با شناسه (edit_text_email) وجود دارد یا نه؟
onView(withId(R.id.edit_text_email)).check(matches(allOf(isDescendantOfA(withId(R.id.layout_login)), isDisplayed())));
میدانم که این سبک نگارش ممکن است کمی پیچیده باشد ولی منطق پشت آن بسیار ساده است. با کمی تمرین و مطالعه مثالهای بیشتر میتوانید به راحتی با همه این متدها آشنا شوید.
به همین سادگی اولین تست ابزاری خودمان را نوشتیم! حالا میخواهیم این تست را اجرا کنیم.
اجرای تست
همانطور که در قسمتهای قبلی درباره تست برنامههای اندروید نوشتیم برای انجام یک تست خاص کافی است کلاس تست را باز کنیم و دکمه سبز رنگ کنار متد تست را کلیک کنیم:
اگر بخواهیم همه تستهای کلاس را اجرا کنیم، دکمه سبز رنگ اجرا که در کنار نام کلاس قرار دارد را کلیک کنیم:
و اگر بخواهیم کل تستهای ابزاری نوشته شده برای پروژه را اجرا کنیم کافی است در نمای Project بر روی شاخه androidTest راست کلیک کنیم و گزینه Run Tests را انتخاب کنیم یا از کلیدهای ترکیبی Ctrl + Shift + F10 استفاده کنیم:
حالا اگر تستی که نوشتیم را اجرا کنیم و همه چیز درست باشد، در پنجره یا نمای run که به صورت خودکار باز میشود نتیجه اجرای تست نمایش داده میشود. توجه کنید که تستهای ابزاری در محیط واقعی اجرا میشوند بنابراین باید یک شبیهساز یا دستگاه اندرویدی متصل به سیستم داشته باشید و بعد تست را اجرا کنید:
حالا اگر در آینده خود شما یا یک برنامهنویس دیگر در تیم شما، تغییری در برنامه بدهد، مثلا شناسه یا ID یک ویو را عوض کند یا کاری کند که کلیک بر روی این دکمه به جای نمایش صفحه Login کاربر را به صفحه دیگری ببرد، اجرای این تست با خطا مواجه شده و به سرعت میتوان اشکال به وجود آمده در برنامه را تشخیص داد. برای این که ببینیم اگر تست اصطلاحا fail شود چه اتفاقی میافتد، فرض کنید بعد از مدتها تصمیم میگیرید با کلیک روی دکمه Login یک پنجره دیالوگ به کاربر نشان بدهید و از او تأیید بگیرید:
loginButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { new AlertDialog.Builder(MainActivity.this) .setTitle("Login") .setMessage("Go to login page?") .setPositiveButton("Ok", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { startActivity(new Intent(MainActivity.this, LoginActivity.class)); } }) .setNegativeButton("Cancel", null) .show(); } });
به همین سادگی تست fail میشود! اگر تست را دوباره اجرا کنیم چه اتفاقی رخ میدهد؟ ببینید:
در مطلب بعدی با تستهای بیشتری آشنا میشویم!
سلام
برای این آموزش خیلی تشکر میکنم .واقعا پایه ای و کاربردی هست.
ولی نیاز هست چند مورد رو ذکر کنم :
اول اینکه در کد ها علامت های به شکل غلط & نمایش داده می شوند که البته در عکس ها صحیح آن رو می توان دید.
و دوم اینکه این تست با خطا مواجه میشه چون در کدهای کلاس تست به id دکمه button_login اشاره شده که مربوط به صفحه login هست و باید با login_button جایگزین شود.
با آرزوی موفقیت برای شما.
ممنون خیلی خوب بود
عالی بود مرسی