دستورهای پخت اسپرسو

این سند نحوه راه‌اندازی انواع آزمایش‌های رایج Espresso را شرح می‌دهد.

مطابقت دادن یک نما در کنار نمای دیگر

یک چیدمان می‌تواند شامل نماهای خاصی باشد که به‌خودی‌خود منحصربه‌فرد نیستند. برای مثال، دکمه تماس تکراری در جدول مخاطبین می‌تواند همان R.id را داشته باشد، حاوی همان نوشتار باشد، و همان ویژگی‌های دکمه‌های تماس دیگر را در سلسله‌مراتب نما داشته باشد.

برای مثال، در این فعالیت، نمای دارای نوشتار "7" در چندین ردیف تکرار می‌شود:

فعالیت فهرستی که ۳ نسخه از یک عنصر نمای یکسان را
     درون فهرستی ۳ موردی نشان می‌دهد

اغلب، نمای غیریکتا با برچسب یکتایی که در کنار آن قرار دارد جفت می‌شود، مثلاً نام مخاطب در کنار دکمه تماس. در این حالت، می‌توانید از تطبیق‌دهنده hasSibling() برای محدود کردن انتخابتان استفاده کنید:

کاتلین

onView(allOf(withText("7"), hasSibling(withText("item: 0"))))
    .perform(click())

جاوا

onView(allOf(withText("7"), hasSibling(withText("item: 0"))))
    .perform(click());

مطابقت دادن نمایی که داخل نوار اقدام است

ActionBarTestActivity دو نوار کنش متفاوت دارد: نوار کنش معمولی و نوار کنش بافتاری که از منوِ گزینه‌ها ایجاد می‌شود. هر دو نوار اقدام یک مورد دارند که همیشه نمایان است و دو مورد دارند که فقط در منوی سرریز نمایان هستند. وقتی روی موردی کلیک می‌شود، TextView را به محتوای مورد کلیک‌شده تغییر می‌دهد.

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

کاتلین

fun testClickActionBarItem() {
    // We make sure the contextual action bar is hidden.
    onView(withId(R.id.hide_contextual_action_bar))
        .perform(click())

    // Click on the icon - we can find it by the r.Id.
    onView(withId(R.id.action_save))
        .perform(click())

    // Verify that we have really clicked on the icon
    // by checking the TextView content.
    onView(withId(R.id.text_action_bar_result))
        .check(matches(withText("Save")))
}

جاوا

public void testClickActionBarItem() {
    // We make sure the contextual action bar is hidden.
    onView(withId(R.id.hide_contextual_action_bar))
        .perform(click());

    // Click on the icon - we can find it by the r.Id.
    onView(withId(R.id.action_save))
        .perform(click());

    // Verify that we have really clicked on the icon
    // by checking the TextView content.
    onView(withId(R.id.text_action_bar_result))
        .check(matches(withText("Save")));
}

دکمه ذخیره در نوار اقدام، در بالای فعالیت قرار دارد

کد برای نوار کنش بافتاری یکسان به‌نظر می‌رسد:

کاتلین

fun testClickActionModeItem() {
    // Make sure we show the contextual action bar.
    onView(withId(R.id.show_contextual_action_bar))
        .perform(click())

    // Click on the icon.
    onView((withId(R.id.action_lock)))
        .perform(click())

    // Verify that we have really clicked on the icon
    // by checking the TextView content.
    onView(withId(R.id.text_action_bar_result))
        .check(matches(withText("Lock")))
}

جاوا

public void testClickActionModeItem() {
    // Make sure we show the contextual action bar.
    onView(withId(R.id.show_contextual_action_bar))
        .perform(click());

    // Click on the icon.
    onView((withId(R.id.action_lock)))
        .perform(click());

    // Verify that we have really clicked on the icon
    // by checking the TextView content.
    onView(withId(R.id.text_action_bar_result))
        .check(matches(withText("Lock")));
}

دکمه قفل در نوار اقدام، در بالای فعالیت قرار دارد

کلیک کردن روی موارد در منوِ سرریز برای نوار اقدام معمولی کمی دشوارتر است، زیرا برخی‌از دستگاه‌ها دکمه منوِ سرریز سخت‌افزاری دارند که موارد سرریز را در منوِ گزینه‌ها باز می‌کند و برخی‌از دستگاه‌ها دکمه منوِ سرریز نرم‌افزاری دارند که منوِ سرریز معمولی را باز می‌کند. خوشبختانه Espresso این کار را برای ما انجام می‌دهد.

برای نوار اقدام معمولی:

کاتلین

fun testActionBarOverflow() {
    // Make sure we hide the contextual action bar.
    onView(withId(R.id.hide_contextual_action_bar))
        .perform(click())

    // Open the options menu OR open the overflow menu, depending on whether
    // the device has a hardware or software overflow menu button.
    openActionBarOverflowOrOptionsMenu(
            ApplicationProvider.getApplicationContext<Context>())

    // Click the item.
    onView(withText("World"))
        .perform(click())

    // Verify that we have really clicked on the icon by checking
    // the TextView content.
    onView(withId(R.id.text_action_bar_result))
        .check(matches(withText("World")))
}

جاوا

public void testActionBarOverflow() {
    // Make sure we hide the contextual action bar.
    onView(withId(R.id.hide_contextual_action_bar))
        .perform(click());

    // Open the options menu OR open the overflow menu, depending on whether
    // the device has a hardware or software overflow menu button.
    openActionBarOverflowOrOptionsMenu(
            ApplicationProvider.getApplicationContext());

    // Click the item.
    onView(withText("World"))
        .perform(click());

    // Verify that we have really clicked on the icon by checking
    // the TextView content.
    onView(withId(R.id.text_action_bar_result))
        .check(matches(withText("World")));
}

دکمه منوِ سرریز نمایان است و فهرستی زیر نوار اقدام در نزدیکی بالای صفحه ظاهر می‌شود

این ویژگی در دستگاه‌های دارای دکمه منوِ سرریز سخت‌افزاری به این صورت است:

دکمه منوِ سرریز وجود ندارد و فهرستی نزدیک پایین صفحه‌نمایش ظاهر می‌شود

برای نوار کنش بافتاری، این کار دوباره بسیار آسان است:

کاتلین

fun testActionModeOverflow() {
    // Show the contextual action bar.
    onView(withId(R.id.show_contextual_action_bar))
        .perform(click())

    // Open the overflow menu from contextual action mode.
    openContextualActionModeOverflowMenu()

    // Click on the item.
    onView(withText("Key"))
        .perform(click())

    // Verify that we have really clicked on the icon by
    // checking the TextView content.
    onView(withId(R.id.text_action_bar_result))
        .check(matches(withText("Key")))
    }
}

جاوا

public void testActionModeOverflow() {
    // Show the contextual action bar.
    onView(withId(R.id.show_contextual_action_bar))
        .perform(click());

    // Open the overflow menu from contextual action mode.
    openContextualActionModeOverflowMenu();

    // Click on the item.
    onView(withText("Key"))
        .perform(click());

    // Verify that we have really clicked on the icon by
    // checking the TextView content.
    onView(withId(R.id.text_action_bar_result))
        .check(matches(withText("Key")));
    }
}

دکمه منو سرریز در نوار اقدام نشان داده می‌شود و فهرست گزینه‌ها در زیر نوار اقدام، نزدیک بالای صفحه نشان داده می‌شود

برای دیدن کد کامل این نمونه‌ها، نمونه ActionBarTest.java را در GitHub مشاهده کنید.

تأیید اینکه نمایی نمایش داده نمی‌شود

پس‌از انجام مجموعه‌ای از کنش‌ها، قطعاً می‌خواهید وضعیت میانای کاربر تحت آزمایش را تأیید کنید. گاهی اوقات، این ممکن است یک مورد منفی باشد، مانند زمانی که چیزی اتفاق نمی‌افتد. به‌خاطر داشته باشید که می‌توانید هر تطبیق‌دهنده نمای hamcrest را بااستفاده از ViewAssertions.matches() به ViewAssertion تبدیل کنید.

در مثال زیر، ما مطابقت‌دهنده isDisplayed() را می‌گیریم و آن را بااستفاده از مطابقت‌دهنده استاندارد not() معکوس می‌کنیم:

کاتلین

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import org.hamcrest.Matchers.not

onView(withId(R.id.bottom_left))
    .check(matches(not(isDisplayed())))

جاوا

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static org.hamcrest.Matchers.not;

onView(withId(R.id.bottom_left))
    .check(matches(not(isDisplayed())));

اگر نما همچنان بخشی از سلسله‌مراتب باشد، رویکرد بالا کار می‌کند. اگر این‌طور نباشد، NoMatchingViewException دریافت خواهید کرد و باید از ViewAssertions.doesNotExist() استفاده کنید.

تأیید اینکه نما وجود ندارد

اگر نما از سلسله‌مراتب نما حذف شده باشد (که می‌تواند زمانی اتفاق بیفتد که کنشی باعث انتقال به فعالیت دیگری شود)، باید از ViewAssertions.doesNotExist() استفاده کنید:

کاتلین

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.matcher.ViewMatchers.withId

onView(withId(R.id.bottom_left))
    .check(doesNotExist())

جاوا

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
import static androidx.test.espresso.matcher.ViewMatchers.withId;

onView(withId(R.id.bottom_left))
    .check(doesNotExist());

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

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

ابتدا مطابقت‌دهنده:

کاتلین

private fun withAdaptedData(dataMatcher: Matcher<Any>): Matcher<View> {
    return object : TypeSafeMatcher<View>() {

        override fun describeTo(description: Description) {
            description.appendText("with class name: ")
            dataMatcher.describeTo(description)
        }

        public override fun matchesSafely(view: View) : Boolean {
            if (view !is AdapterView<*>) {
                return false
            }

            val adapter = view.adapter
            for (i in 0 until adapter.count) {
                if (dataMatcher.matches(adapter.getItem(i))) {
                    return true
                }
            }

            return false
        }
    }
}

جاوا

private static Matcher<View> withAdaptedData(final Matcher<Object> dataMatcher) {
    return new TypeSafeMatcher<View>() {

        @Override
        public void describeTo(Description description) {
            description.appendText("with class name: ");
            dataMatcher.describeTo(description);
        }

        @Override
        public boolean matchesSafely(View view) {
            if (!(view instanceof AdapterView)) {
                return false;
            }

            @SuppressWarnings("rawtypes")
            Adapter adapter = ((AdapterView) view).getAdapter();
            for (int i = 0; i < adapter.getCount(); i++) {
                if (dataMatcher.matches(adapter.getItem(i))) {
                    return true;
                }
            }

            return false;
        }
    };
}

سپس تنها چیزی که نیاز داریم onView() است تا AdapterView را پیدا کنیم:

کاتلین

fun testDataItemNotInAdapter() {
    onView(withId(R.id.list))
          .check(matches(not(withAdaptedData(withItemContent("item: 168")))))
    }
}

جاوا

@SuppressWarnings("unchecked")
public void testDataItemNotInAdapter() {
    onView(withId(R.id.list))
          .check(matches(not(withAdaptedData(withItemContent("item: 168")))));
    }
}

و ما یک ادعا داریم که اگر موردی برابر با «مورد: ۱۶۸» در نمای آداپتور با فهرست شناسه وجود داشته باشد، ناموفق خواهد بود.

برای دیدن نمونه کامل، به روش testDataItemNotInAdapter() در کلاس AdapterViewTest.java در GitHub نگاه کنید.

استفاده از گرداننده خرابی سفارشی

جایگزین کردن FailureHandler پیش‌فرض در Espresso با یک سفارشی امکان رسیدگی به خطاهای اضافی یا متفاوت را فراهم می‌کند، مانند گرفتن نماگرفت یا ارسال اطلاعات اشکال‌زدایی اضافی.

مثال CustomFailureHandlerTest نشان می‌دهد که چگونه یک کنترل‌کننده خرابی سفارشی را پیاده‌سازی کنید:

کاتلین

private class CustomFailureHandler(targetContext: Context) : FailureHandler {
    private val delegate: FailureHandler

    init {
        delegate = DefaultFailureHandler(targetContext)
    }

    override fun handle(error: Throwable, viewMatcher: Matcher<View>) {
        try {
            delegate.handle(error, viewMatcher)
        } catch (e: NoMatchingViewException) {
            throw MySpecialException(e)
        }

    }
}

جاوا

private static class CustomFailureHandler implements FailureHandler {
    private final FailureHandler delegate;

    public CustomFailureHandler(Context targetContext) {
        delegate = new DefaultFailureHandler(targetContext);
    }

    @Override
    public void handle(Throwable error, Matcher<View> viewMatcher) {
        try {
            delegate.handle(error, viewMatcher);
        } catch (NoMatchingViewException e) {
            throw new MySpecialException(e);
        }
    }
}

این کنترل‌کننده خطا به‌جای NoMatchingViewException، MySpecialException را برمی‌گرداند و همه خطاهای دیگر را به DefaultFailureHandler واگذار می‌کند. CustomFailureHandler را می‌توان با Espresso در روش setUp() آزمایش ثبت کرد:

کاتلین

@Throws(Exception::class)
override fun setUp() {
    super.setUp()
    getActivity()
    setFailureHandler(CustomFailureHandler(
            ApplicationProvider.getApplicationContext<Context>()))
}

جاوا

@Override
public void setUp() throws Exception {
    super.setUp();
    getActivity();
    setFailureHandler(new CustomFailureHandler(
            ApplicationProvider.getApplicationContext()));
}

برای اطلاعات بیشتر، به میانای FailureHandler و Espresso.setFailureHandler() مراجعه کنید.

هدف‌گیری پنجره‌های غیرپیش‌فرض

Android از چند پنجره پشتیبانی می‌کند. معمولاً این موضوع برای کاربران و توسعه‌دهنده نرم‌افزار شفاف است، اما در برخی موارد چندین پنجره قابل‌مشاهده است، مثلاً وقتی پنجره تکمیل خودکار روی پنجره اصلی برنامه در ابزاره جستجو کشیده می‌شود. برای ساده کردن کارها، Espresso به‌طور پیش‌فرض از روشی اکتشافی استفاده می‌کند تا حدس بزند قصد دارید با کدام Window تعامل داشته باشید. این روش اکتشافی تقریباً همیشه کافی است؛ بااین‌حال، در موارد نادر، باید مشخص کنید که تعامل کدام پنجره را هدف قرار دهد. می‌توانید این کار را با ارائه تطبیق‌دهنده پنجره ریشه خودتان یا تطبیق‌دهنده Root انجام دهید:

کاتلین

onView(withText("South China Sea"))
    .inRoot(withDecorView(not(`is`(getActivity().getWindow().getDecorView()))))
    .perform(click())

جاوا

onView(withText("South China Sea"))
    .inRoot(withDecorView(not(is(getActivity().getWindow().getDecorView()))))
    .perform(click());

همانند ViewMatchers، مجموعه‌ای از RootMatchers ازپیش ارائه‌شده را ارائه می‌دهیم. البته همیشه می‌توانید شیء Matcher خودتان را پیاده‌سازی کنید.

به نمونه MultipleWindowTest در GitHub نگاهی بیندازید.

سرصفحه‌ها و پانویس‌ها بااستفاده از روش‌های addHeaderView() و addFooterView() به ListViews اضافه می‌شوند. برای اطمینان از اینکه Espresso.onData() می‌داند کدام شیء داده را مطابقت دهد، حتماً مقدار شیء داده ازپیش‌تنظیم‌شده را به‌عنوان پارامتر دوم به addHeaderView() و addFooterView() ارسال کنید. برای مثال:

کاتلین

const val FOOTER = "FOOTER"
...
val footerView = layoutInflater.inflate(R.layout.list_item, listView, false)
footerView.findViewById<TextView>(R.id.item_content).text = "count:"
footerView.findViewById<TextView>(R.id.item_size).text
        = data.size.toString
listView.addFooterView(footerView, FOOTER, true)

جاوا

public static final String FOOTER = "FOOTER";
...
View footerView = layoutInflater.inflate(R.layout.list_item, listView, false);
footerView.findViewById<TextView>(R.id.item_content).setText("count:");
footerView.findViewById<TextView>(R.id.item_size).setText(String.valueOf(data.size()));
listView.addFooterView(footerView, FOOTER, true);

سپس می‌توانید مطابقت‌دهنده‌ای برای پانویس بنویسید:

کاتلین

import org.hamcrest.Matchers.allOf
import org.hamcrest.Matchers.instanceOf
import org.hamcrest.Matchers.`is`

fun isFooter(): Matcher<Any> {
    return allOf(`is`(instanceOf(String::class.java)),
            `is`(LongListActivity.FOOTER))
}

جاوا

import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;

@SuppressWarnings("unchecked")
public static Matcher<Object> isFooter() {
    return allOf(is(instanceOf(String.class)), is(LongListActivity.FOOTER));
}

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

کاتلین

import androidx.test.espresso.Espresso.onData
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.sample.LongListMatchers.isFooter

fun testClickFooter() {
    onData(isFooter())
        .perform(click())

    // ...
}

جاوا

import static androidx.test.espresso.Espresso.onData;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.sample.LongListMatchers.isFooter;

public void testClickFooter() {
    onData(isFooter())
        .perform(click());

    // ...
}

نمونه کد کامل را که در روش testClickFooter() AdapterViewTest.java در GitHub قرار دارد ببینید.