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

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

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

يمكن أن يحتوي التخطيط على طرق عرض معينة لا تكون فريدة في حد ذاتها. بالنسبة مثلاً، يمكن أن يكون زر الاتصال المتكرر في جدول جهات الاتصال مطابقًا 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")));
}

يوجد زر القفل على شريط الإجراءات، في أعلى النشاط

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

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

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 على جيت هب.

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

يمكنك استبدال 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 باستخدام قهوة الإسبريسو بطريقة الاختبار 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 استخدام نوافذ متعددة. في العادة، يكون هذا واضحًا للمستخدمين ومطوّر التطبيق، إلا أنه في بعض الحالات تظهر نوافذ متعددة، مثل كما هو الحال عندما يتم رسم نافذة إكمال تلقائي فوق نافذة التطبيق الرئيسية في أداة البحث. لتبسيط الأمور، تستخدم قهوة الإسبريسو بشكل افتراضي إرشادات تخمين أي 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 الخاص بك.

ألق نظرة على MultipleWindowTest عيّنة على 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.