وصفات الإسبريسو

يوضّح هذا المستند كيفية إعداد مجموعة متنوعة من اختبارات الإسبريسو الشائعة.

مطابقة طريقة عرض بجانب طريقة عرض أخرى

يمكن أن يحتوي التخطيط على طرق عرض معينة ليست فريدة في حد ذاتها. على سبيل المثال، يمكن أن يحتوي زر الاتصال المتكرر في جدول جهات الاتصال على نفس R.id، وأن يحتوي على النص نفسه، وأن يكون له نفس خصائص أزرار الاتصال الأخرى ضمن التدرّج الهرمي للعرض.

على سبيل المثال، في هذا النشاط، تتكرر طريقة العرض التي تتضمن النص "7" في عدة صفوف:

نشاط قائمة يعرض 3 نسخ من عنصر العرض نفسه داخل قائمة من 3 عناصر

وفي كثير من الأحيان، سيتم إقران طريقة العرض غير الفريدة مع بعض التصنيفات الفريدة التي تقع بجانبها، مثل اسم جهة الاتصال بجوار زر الاتصال. في هذه الحالة، يمكنك استخدام مُطابق hasSibling() لتضييق نطاق التحديد:

Kotlin

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

Java

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

مطابقة طريقة عرض داخل شريط الإجراءات

يحتوي ActionBarTestActivity على شريطَي إجراءات مختلفَين: شريط إجراءات عادي وشريط إجراءات سياقي يتم إنشاؤه من قائمة الخيارات. يحتوي كلا شريطي الإجراءات على عنصر واحد مرئي دائمًا وعنصران مرئيان فقط في القائمة الكاملة. عند النقر على عنصر، يتم تغيير TextView إلى محتوى العنصر الذي تم النقر عليه.

ومن السهل أيضًا مطابقة الرموز المرئية في كلٍ من شريطي الإجراءات، كما هو موضّح في مقتطف الرمز التالي:

Kotlin

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")))
}

Java

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")));
}

يظهر زر الحفظ في شريط الإجراءات في أعلى النشاط.

يبدو الرمز متطابقًا مع شريط الإجراءات السياقي:

Kotlin

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")))
}

Java

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 مع ذلك بالنسبة لنا.

بالنسبة إلى شريط الإجراءات العادي:

Kotlin

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")))
}

Java

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")));
}

يكون زر القائمة الكاملة مرئيًا، وتظهر قائمة أسفل
          شريط الإجراءات بالقرب من أعلى الشاشة.

إليك كيف يظهر هذا على الأجهزة التي تحتوي على زر القائمة الكاملة للأجهزة:

لا يتوفر زر القائمة الكاملة، وستظهر قائمة بالقرب من أسفل
          الشاشة.

أما بالنسبة إلى شريط الإجراءات السياقي، فالأمر سهل مرة أخرى:

Kotlin

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")))
    }
}

Java

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.

التأكيد على عدم ظهور العرض

بعد تنفيذ سلسلة من الإجراءات، ستحتاج بالتأكيد إلى تأكيد حالة واجهة المستخدم التي تخضع للاختبار. في بعض الأحيان، قد تكون هذه حالة سلبية، مثل عند عدم حدوث شيء ما. تجدر الإشارة إلى أنه يمكنك تحويل أي أداة مطابقة طرق عرض لعبة إلى ViewAssertion باستخدام ViewAssertions.matches().

في المثال أدناه، نأخذ مُطابق isDisplayed() ونعكسه باستخدام مُطابق not() القياسي:

Kotlin

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())))

Java

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():

Kotlin

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())

Java

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 ثم نستخدم مُطابقًا آخر للعمل على البيانات داخل العرض.

المُطابق أولاً:

Kotlin

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
        }
    }
}

Java

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:

Kotlin

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

Java

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

لدينا تأكيد لن ينجح في حال تواجد عنصر يساوي "item: 168" في عرض المحول مع قائمة المعرفات.

للاطّلاع على العيّنة الكاملة، يمكنك الاطّلاع على الطريقة testDataItemNotInAdapter() ضمن الفئة AdapterViewTest.java على GitHub.

استخدام معالِج مخصّص للإخفاق

ومن خلال استبدال FailureHandler التلقائي في Espresso بأخرى مخصّصة، يمكنك معالجة المزيد من الأخطاء، مثل أخذ لقطة شاشة أو تمرير معلومات تصحيح أخطاء إضافية.

يوضح مثال CustomFailureHandlerTest كيفية تنفيذ معالج مخصّص للإخفاقات:

Kotlin

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)
        }

    }
}

Java

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);
        }
    }
}

يطرح معالج الأخطاء هذا MySpecialException بدلاً من NoMatchingViewException ويفوّض جميع عمليات الإخفاق الأخرى إلى DefaultFailureHandler. يمكن تسجيل CustomFailureHandler لدى Espresso من خلال طريقة setUp() للاختبار:

Kotlin

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

Java

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

لمزيد من المعلومات، يُرجى الاطّلاع على واجهة FailureHandler و Espresso.setFailureHandler().

استهداف النوافذ غير التلقائية

يتيح Android استخدام عدة نوافذ. عادةً ما تكون هذه المعلومات واضحة للمستخدمين ومطوّر التطبيقات، ولكن في بعض الحالات تظهر نوافذ متعددة، على سبيل المثال عندما يتم رسم نافذة الإكمال التلقائي فوق نافذة التطبيق الرئيسية في أداة البحث. لتبسيط الأمور، يستخدم Espresso تلقائيًا إرشاديًا لتخمين Window الذي تنوي التفاعل معه. تكاد تكون هذه الإرشادات مفيدة على الدوام، ولكن في حالات نادرة، ستحتاج إلى تحديد النافذة التي يجب أن يستهدفها التفاعل. ويمكنك إجراء ذلك من خلال توفير أداة مطابقة نافذة الجذر الخاصة بك أو أداة مطابقة Root:

Kotlin

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

Java

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

وكما هي الحال مع السمة ViewMatchers، نوفّر مجموعة من رموز RootMatchers التي يتم تقديمها مسبقًا. يمكنك في أي وقت تنفيذ كائن Matcher الخاص بك.

يمكنك إلقاء نظرة على نموذج MultiWindowTest على GitHub.

تتم إضافة الرؤوس والتذييلات إلى ListViews باستخدام الطريقتَين addHeaderView() وaddFooterView(). لضمان أن يعرف Espresso.onData() كائن البيانات المطلوب مطابقته، احرص على تمرير قيمة كائن البيانات المُعدّة مسبقًا كمَعلمة ثانية إلى addHeaderView() وaddFooterView(). مثلاً:

Kotlin

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)

Java

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);

بعد ذلك، يمكنك كتابة مطابقة للتذييل:

Kotlin

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))
}

Java

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));
}

ويعد تحميل العرض في الاختبار أمرًا بسيطًا:

Kotlin

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())

    // ...
}

Java

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.