مبانی اسپرسو

این سند نحوه تکمیل کردن تکالیف آزمایش خودکار رایج را بااستفاده از Espresso API توضیح می‌دهد.

Espresso API نویسندگان آزمون را تشویق می‌کند تا از دیدگاه کارهایی که کاربر ممکن است هنگام تعامل با برنامه انجام دهد فکر کنند - پیدا کردن عناصر میانای کاربر و تعامل با آن‌ها. درعین‌حال، این چارچوب از دسترسی مستقیم به فعالیت‌ها و نماهای برنامه جلوگیری می‌کند، زیرا نگه‌داشتن این اشیاء و کار کردن روی آن‌ها خارج از رشته میانای کاربر، منبع اصلی ناپایداری آزمایش است. بنابراین، روش‌هایی مثل getView() و getCurrentActivity() را در Espresso API نخواهید دید. با پیاده‌سازی زیرکلاس‌های خودتان از ViewAction و ViewAssertion، همچنان می‌توانید به‌طور ایمن روی نماها کار کنید.

مؤلفه‌های میانای برنامه‌سازی کاربردی

مؤلفه‌های اصلی Espresso شامل موارد زیر است:

  • Espresso – نقطه ورود به تعاملات با نماها (ازطریق onView() و onData()). همچنین میاناهای برنامه‌سازی کاربردی را که لزوماً به هیچ نمایی مرتبط نیستند، مانند pressBack()، نمایش می‌دهد.
  • ViewMatchers – مجموعه‌ای از اشیاء که رابط Matcher<? super View> را پیاده‌سازی می‌کنند. می‌توانید یک یا چند مورد از این‌ها را به روش onView() ارسال کنید تا نمایی را در سلسله‌مراتب نمای فعلی پیدا کنید.
  • ViewActions – مجموعه‌ای از اشیاء ViewAction که می‌توانند به روش ViewInteraction.perform() منتقل شوند، مانند click().
  • ViewAssertions – مجموعه‌ای از اشیاء ViewAssertion که می‌توانند به روش ViewInteraction.check() منتقل شوند. در بیشتر مواقع، از ادعای matches استفاده خواهید کرد که از View matcher برای ادعای وضعیت نمای انتخاب‌شده فعلی استفاده می‌کند.

مثال:

کاتلین

// withId(R.id.my_view) is a ViewMatcher
// click() is a ViewAction
// matches(isDisplayed()) is a ViewAssertion
onView(withId(R.id.my_view))
    .perform(click())
    .check(matches(isDisplayed()))

جاوا

// withId(R.id.my_view) is a ViewMatcher
// click() is a ViewAction
// matches(isDisplayed()) is a ViewAssertion
onView(withId(R.id.my_view))
    .perform(click())
    .check(matches(isDisplayed()));

پیدا کردن نما

در اکثر موارد، روش onView() یک تطبیق‌دهنده hamcrest می‌گیرد که انتظار می‌رود با یک — و فقط یک — نما در سلسله‌مراتب نمای فعلی مطابقت داشته باشد. تطبیق‌دهنده‌ها قدرتمند هستند و برای کسانی که از آن‌ها با Mockito یا JUnit استفاده کرده‌اند آشنا خواهند بود. اگر با تطبیق‌دهنده‌های hamcrest آشنا نیستید، پیشنهاد می‌کنیم با نگاهی سریع به این ارائه شروع کنید.

اغلب نمای موردنظر دارای R.id منحصربه‌فردی است و تطبیق‌دهنده withId ساده‌ای جستجوی نما را محدود می‌کند. بااین‌حال، موارد قانونی زیادی وجود دارد که نمی‌توانید R.id را در زمان توسعه آزمایش تعیین کنید. برای مثال، نمای خاص ممکن است R.id نداشته باشد یا R.id منحصربه‌فرد نباشد. این امر می‌تواند نوشتن آزمایش‌های ابزار دقیق عادی را شکننده و پیچیده کند، زیرا روش عادی برای دسترسی به نما (با findViewById()) کار نمی‌کند. بنابراین، ممکن است لازم باشد به اعضای خصوصی «فعالیت» یا «قطعه» حاوی نما دسترسی پیدا کنید یا محفظه‌ای با R.id شناخته‌شده پیدا کنید و برای نمای خاص به محتوای آن پیمایش کنید.

Espresso این مشکل را به‌طور تمیز با اجازه دادن به شما برای محدود کردن نما بااستفاده از اشیاء ViewMatcher موجود یا اشیاء سفارشی خودتان حل می‌کند.

پیدا کردن نما براساس R.id آن به‌سادگی فراخوانی onView() است:

کاتلین

onView(withId(R.id.my_view))

جاوا

onView(withId(R.id.my_view));

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

java.lang.RuntimeException:
androidx.test.espresso.AmbiguousViewMatcherException
This matcher matches multiple views in the hierarchy: (withId: is <123456789>)

...

+----->SomeView{id=123456789, res-name=plus_one_standard_ann_button,
visibility=VISIBLE, width=523, height=48, has-focus=false, has-focusable=true,
window-focus=true, is-focused=false, is-focusable=false, enabled=true,
selected=false, is-layout-requested=false, text=,
root-is-layout-requested=false, x=0.0, y=625.0, child-count=1}
****MATCHES****
|
+------>OtherView{id=123456789, res-name=plus_one_standard_ann_button,
visibility=VISIBLE, width=523, height=48, has-focus=false, has-focusable=true,
window-focus=true, is-focused=false, is-focusable=true, enabled=true,
selected=false, is-layout-requested=false, text=Hello!,
root-is-layout-requested=false, x=0.0, y=0.0, child-count=1}
****MATCHES****

با بررسی مشخصه‌های مختلف نماها، ممکن است ویژگی‌های منحصربه‌فردی را پیدا کنید که قابل‌شناسایی باشند. در مثال بالا، یکی از نماها دارای نوشتار "Hello!" است. می‌توانید از این مورد برای محدود کردن جستجو بااستفاده از مطابقت‌دهنده‌های ترکیبی استفاده کنید:

کاتلین

onView(allOf(withId(R.id.my_view), withText("Hello!")))

جاوا

onView(allOf(withId(R.id.my_view), withText("Hello!")));

همچنین می‌توانید انتخاب کنید که هیچ‌کدام از مطابقت‌دهنده‌ها را معکوس نکنید:

کاتلین

onView(allOf(withId(R.id.my_view), not(withText("Unwanted"))))

جاوا

onView(allOf(withId(R.id.my_view), not(withText("Unwanted"))));

برای مشاهده تطبیق‌دهنده‌های نمای ارائه‌شده توسط Espresso، ViewMatchers را ببینید.

ملاحظات

  • در برنامه‌ای که عملکرد خوبی دارد، همه نماهایی که کاربر می‌تواند با آن‌ها تعامل داشته باشد باید حاوی نوشتار توصیفی باشند یا شرح محتوا داشته باشند. برای جزئیات بیشتر، به افزایش دسترس‌پذیری برنامه‌ها مراجعه کنید. اگر نمی‌توانید جستجویی را بااستفاده از withText() یا withContentDescription() محدود کنید، آن را به‌عنوان اشکال دسترس‌پذیری درنظر بگیرید.
  • از کمترین تطبیق‌دهنده توصیفی که نمای موردنظرتان را پیدا می‌کند استفاده کنید. بیش‌ازحد مشخص نکنید زیرا این کار باعث می‌شود چارچوب بیشتر از حد لازم کار کند. برای مثال، اگر نمایشی به‌طور منحصربه‌فرد ازطریق نوشتارش قابل‌شناسایی باشد، نیازی نیست مشخص کنید که نمایش از TextView نیز قابل‌اختصاص است. برای بسیاری از نماها، R.id نما باید کافی باشد.
  • اگر نمای هدف داخل AdapterView باشد—مثل ListView، GridView، یا Spinner—روش onView() ممکن است کار نکند. در این موارد، باید درعوض از onData() استفاده کنید.

انجام کنش روی نما

وقتی مطابقت‌دهنده مناسبی برای نمای هدف پیدا کردید، می‌توانید بااستفاده از روش perform، نمونه‌های ViewAction را روی آن اجرا کنید.

برای مثال، برای کلیک کردن روی نما:

کاتلین

onView(...).perform(click())

جاوا

onView(...).perform(click());

می‌توانید با یک فراخوانی اجرا، بیش‌از یک کنش را اجرا کنید:

کاتلین

onView(...).perform(typeText("Hello"), click())

جاوا

onView(...).perform(typeText("Hello"), click());

اگر نمایی که با آن کار می‌کنید در داخل ScrollView (عمودی یا افقی) قرار دارد، کنش‌های قبلی را که نیاز به نمایش نما دارند (مانند click() و typeText()) با scrollTo() درنظر بگیرید. این اطمینان می‌دهد که نما قبل‌از ادامه دادن به کنش دیگر نمایش داده می‌شود:

کاتلین

onView(...).perform(scrollTo(), click())

جاوا

onView(...).perform(scrollTo(), click());

برای مشاهده کنش‌های ارائه‌شده توسط Espresso، ViewActions را ببینید.

بررسی ادعاهای نما

بااستفاده از روش check() می‌توان ادعاها را بر نمای انتخاب‌شده فعلی اعمال کرد. پرکاربردترین ادعا، ادعای matches() است. از شیء ViewMatcher برای تأیید وضعیت نمای انتخاب‌شده فعلی استفاده می‌کند.

برای مثال، برای بررسی اینکه آیا یک نما دارای نوشتار "Hello!" است:

کاتلین

onView(...).check(matches(withText("Hello!")))

جاوا

onView(...).check(matches(withText("Hello!")));

اگر می‌خواهید ادعا کنید که "Hello!" محتوای نما است، موارد زیر روال‌های نامطلوب درنظر گرفته می‌شود:

کاتلین

// Don't use assertions like withText inside onView.
onView(allOf(withId(...), withText("Hello!"))).check(matches(isDisplayed()))

جاوا

// Don't use assertions like withText inside onView.
onView(allOf(withId(...), withText("Hello!"))).check(matches(isDisplayed()));

ازسوی دیگر، اگر می‌خواهید ادعا کنید که نمایی با نوشتار "Hello!" نمایان است (برای مثال پس‌از تغییر پرچم رؤیت‌پذیری نماها)، کد درست است.

مشاهده آزمایش ساده ادعا

در این مثال، SimpleActivity حاوی Button و TextView است. وقتی روی دکمه کلیک شود، محتوای TextView به "Hello Espresso!" تغییر می‌کند.

نحوه آزمایش این مورد با Espresso در اینجا آمده است:

روی دکمه کلیک کنید

اولین قدم این است که به‌دنبال ویژگی‌ای بگردید که به پیدا کردن دکمه کمک می‌کند. دکمه در SimpleActivity دارای R.id یکتا است، همان‌طور که انتظار می‌رفت.

کاتلین

onView(withId(R.id.button_simple))

جاوا

onView(withId(R.id.button_simple));

اکنون برای کلیک کردن:

کاتلین

onView(withId(R.id.button_simple)).perform(click())

جاوا

onView(withId(R.id.button_simple)).perform(click());

درستی‌سنجی کردن نوشتار TextView

TextView با نوشتار برای درستی‌سنجی نیز دارای R.id منحصربه‌فرد است:

کاتلین

onView(withId(R.id.text_simple))

جاوا

onView(withId(R.id.text_simple));

اکنون برای درستی‌سنجی نوشتار محتوا:

کاتلین

onView(withId(R.id.text_simple)).check(matches(withText("Hello Espresso!")))

جاوا

onView(withId(R.id.text_simple)).check(matches(withText("Hello Espresso!")));

بررسی بار کردن داده‌ها در نماهای آداپتور

AdapterView نوع خاصی از ابزاره است که داده‌هایش را به‌طور پویا از «آداپتور» بار می‌کند. رایج‌ترین نمونه AdapterView، ListView است. برخلاف ابزاره‌های ایستا مثل LinearLayout، فقط زیرمجموعه‌ای از AdapterView فرزندان ممکن است در سلسله‌مراتب نمای فعلی بار شود. جستجوی ساده onView() نماهایی را که درحال‌حاضر بار نشده‌اند پیدا نمی‌کند.

Espresso این مشکل را با ارائه یک نقطه ورود جداگانه onData() حل می‌کند که می‌تواند ابتدا مورد آداپتور موردنظر را بارگیری کند و قبل‌از انجام عملیات روی آن یا هریک از فرزندانش، آن را در کانون توجه قرار دهد.

هشدار: پیاده‌سازی‌های سفارشی AdapterView ممکن است با روش onData() مشکل داشته باشند اگر قراردادهای ارث‌بری را نقض کنند، به‌ویژه getItem() API. در این موارد، بهترین اقدام این است که کد برنامه‌تان را بازسازی کنید. اگر نمی‌توانید این کار را انجام دهید، می‌توانید یک AdapterViewProtocol سفارشی منطبق را پیاده‌سازی کنید. برای اطلاعات بیشتر، کلاس پیش‌فرض AdapterViewProtocols ارائه‌شده توسط Espresso را ببینید.

آزمایش ساده نمای آداپتور

این آزمایش ساده نحوه استفاده از onData() را نشان می‌دهد. SimpleActivity حاوی Spinner با چند مورد است که انواع نوشیدنی‌های قهوه را نشان می‌دهد. وقتی موردی انتخاب می‌شود، TextView به "One %s a day!" تغییر می‌کند، که در آن %s نشان‌دهنده مورد انتخاب‌شده است.

هدف این آزمایش باز کردن Spinner، انتخاب یک مورد خاص، و تأیید اینکه TextView حاوی آن مورد است می‌باشد. ازآنجایی‌که کلاس Spinner براساس AdapterView است، توصیه می‌شود برای مطابقت دادن مورد، به‌جای onView() از onData() استفاده کنید.

باز کردن انتخاب مورد

کاتلین

onView(withId(R.id.spinner_simple)).perform(click())

جاوا

onView(withId(R.id.spinner_simple)).perform(click());

انتخاب مورد

برای انتخاب مورد، Spinner یک ListView با محتوای آن ایجاد می‌کند. این نما می‌تواند بسیار طولانی باشد و ممکن است عنصر به سلسله‌مراتب نما اضافه نشود. بااستفاده از onData()، عنصر دلخواهمان را به سلسله‌مراتب نما تحمیل می‌کنیم. موارد موجود در Spinner رشته هستند، بنابراین می‌خواهیم موردی را مطابقت دهیم که با رشته "Americano" برابر باشد:

کاتلین

onData(allOf(`is`(instanceOf(String::class.java)),
        `is`("Americano"))).perform(click())

جاوا

onData(allOf(is(instanceOf(String.class)), is("Americano"))).perform(click());

تأیید کنید نوشتار درست باشد

کاتلین

onView(withId(R.id.spinnertext_simple))
    .check(matches(withText(containsString("Americano"))))

جاوا

onView(withId(R.id.spinnertext_simple))
    .check(matches(withText(containsString("Americano"))));

اشکال‌زدایی

Espresso اطلاعات اشکال‌زدایی مفیدی را درصورت ناموفق بودن آزمایش ارائه می‌دهد:

گزارش‌گیری

Espresso همه کنش‌های مشاهده را در logcat ثبت می‌کند. برای مثال:

ViewInteraction: Performing 'single click' action on view with text: Espresso

مشاهده سلسله‌مراتب

وقتی onView() ناموفق باشد، Espresso سلسله‌مراتب نما را در پیام استثنا چاپ می‌کند.

  • اگر onView() نمای هدف را پیدا نکند، NoMatchingViewException ایجاد می‌شود. می‌توانید سلسله‌مراتب نما را در رشته استثنا بررسی کنید تا تحلیل کنید چرا مطابقت‌دهنده با هیچ نمایی مطابقت نداشت.
  • اگر onView() چندین نما پیدا کند که با تطبیق‌دهنده داده‌شده مطابقت داشته باشد، AmbiguousViewMatcherException ایجاد می‌شود. سلسله‌مراتب نما چاپ می‌شود و همه نماهایی که مطابقت داشتند با برچسب MATCHES علامت‌گذاری می‌شوند:
java.lang.RuntimeException:
androidx.test.espresso.AmbiguousViewMatcherException
This matcher matches multiple views in the hierarchy: (withId: is <123456789>)

...

+----->SomeView{id=123456789, res-name=plus_one_standard_ann_button,
visibility=VISIBLE, width=523, height=48, has-focus=false, has-focusable=true,
window-focus=true, is-focused=false, is-focusable=false, enabled=true,
selected=false, is-layout-requested=false, text=,
root-is-layout-requested=false, x=0.0, y=625.0, child-count=1}
****MATCHES****
|
+------>OtherView{id=123456789, res-name=plus_one_standard_ann_button,
visibility=VISIBLE, width=523, height=48, has-focus=false, has-focusable=true,
window-focus=true, is-focused=false, is-focusable=true, enabled=true,
selected=false, is-layout-requested=false, text=Hello!,
root-is-layout-requested=false, x=0.0, y=0.0, child-count=1}
****MATCHES****

هنگام مواجهه با سلسله‌مراتب نمای پیچیده یا رفتار غیرمنتظره ابزارک‌ها، همیشه استفاده از Hierarchy Viewer در Android Studio برای توضیح مفید است.

هشدارهای نمای آداپتور

Espresso به کاربران درباره وجود ابزاره‌های AdapterView هشدار می‌دهد. وقتی عملیات onView() NoMatchingViewException را برمی‌گرداند و ابزاره‌های AdapterView در سلسله‌مراتب نما وجود دارند، رایج‌ترین راه‌حل استفاده از onData() است. پیام استثنا شامل هشداری با فهرستی از نماهای آداپتور خواهد بود. می‌توانید از این اطلاعات برای فراخوانی onData() برای بار کردن نمای هدف استفاده کنید.

منابع بیشتر

برای اطلاعات بیشتر درباره استفاده از Espresso در آزمایش‌های Android، به منابع زیر مراجعه کنید.

نمونه‌ها