The Android Developer Challenge is back! Submit your idea before December 2.

Urutan langkah menggunakan Espresso

Dokumen ini menjelaskan cara menyiapkan berbagai pengujian Espresso umum.

Mencocokkan tampilan di sebelah tampilan lain

Tata letak dapat berisi tampilan tertentu yang tidak unik dengan sendirinya. Misalnya, tombol panggilan berulang dalam tabel kontak dapat memiliki R.id sama, berisi teks sama, dan memiliki properti sama seperti tombol panggilan lainnya dalam hierarki tampilan.

Misalnya, dalam aktivitas ini, tampilan dengan teks "7" berulang di beberapa baris:

Aktivitas daftar yang menampilkan 3 salinan elemen tampilan yang sama di dalam 3 daftar item

Seringkali, tampilan non-unik akan dipasangkan dengan beberapa label unik yang terletak di sebelahnya, seperti nama kontak di sebelah tombol panggilan. Dalam hal ini, Anda dapat menggunakan matcher hasSibling() untuk mempersempit pilihan:

Kotlin

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

Java

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

Mencocokkan tampilan yang ada di dalam panel tindakan

ActionBarTestActivity memiliki dua panel tindakan yang berbeda: panel tindakan normal dan panel tindakan kontekstual yang dibuat dari menu opsi. Kedua panel tindakan memiliki satu item yang selalu terlihat dan dua item yang hanya terlihat di menu tambahan. Saat sebuah item diklik, TextView akan berubah menjadi konten dari item yang diklik.

Mencocokkan ikon yang terlihat di kedua panel tindakan sangatlah sederhana, seperti yang ditunjukkan dalam cuplikan kode berikut:

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

Tombol simpan di panel tindakan, di bagian atas aktivitas

Kode terlihat identik untuk panel tindakan kontekstual:

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

Tombol kunci di panel tindakan, di bagian atas aktivitas

Mengklik item dalam menu tambahan terlihat agak sulit bagi panel tindakan normal karena beberapa perangkat memiliki tombol menu tambahan hardware, yang membuka item tambahan di menu opsi, dan beberapa perangkat memiliki tombol menu tambahan software, yang membuka menu tambahan normal. Untungnya, Espresso dapat menangani hal-hal tersebut.

Untuk panel tindakan normal:

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

Tombol menu tambahan dapat dilihat, dan daftar muncul di bawah panel tindakan di dekat bagian atas layar

Ini adalah tampilan pada perangkat dengan tombol menu tambahan hardware:

Tidak ada tombol menu tambahan, dan daftar muncul di dekat bagian bawah layar

Untuk panel tindakan kontekstual, sangatlah mudah, cukup:

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

Tombol menu tambahan muncul di panel tindakan, dan daftar opsi muncul di bawah panel tindakan, di dekat bagian atas layar

Untuk melihat kode lengkap dari contoh-contoh tersebut, lihat contoh ActionBarTest.java di GitHub.

Menegaskan bahwa tampilan tidak ditampilkan

Setelah menjalankan serangkaian tindakan, Anda tentu ingin menegaskan status UI yang sedang diuji. Terkadang, langkah ini dapat menjadi kasus negatif, seperti ketika sesuatu tidak terjadi. Ingatlah bahwa Anda dapat mengubah matcher tampilan hamcrest menjadi ViewAssertion dengan menggunakan ViewAssertions.matches().

Dalam contoh di bawah ini, kita mengambil matcher isDisplayed() dan membalikkannya menggunakan matcher 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())));
    

Pendekatan di atas berfungsi jika tampilan masih menjadi bagian dari hierarki. Jika tidak, Anda akan melihat NoMatchingViewException dan perlu menggunakan ViewAssertions.doesNotExist().

Menegaskan bahwa suatu tampilan tidak ada

Jika tampilan dihapus dari hierarki tampilan—yang dapat terjadi saat suatu tindakan menyebabkan transisi ke aktivitas lain—Anda harus menggunakan 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());
    

Menegaskan bahwa item data tidak ada dalam adapter

Untuk membuktikan item data tertentu tidak ada dalam AdapterView Anda harus melakukan beberapa hal yang sedikit berbeda. Kita harus menemukan AdapterView yang diminati dan menginterogasi data yang disimpannya. Kita tidak perlu menggunakan onData(). Sebagai gantinya, kita menggunakan onView() untuk menemukan AdapterView lalu menggunakan matcher lain untuk bekerja pada data di dalam tampilan.

Pertama, matcher-nya:

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

Kemudian, yang kita butuhkan adalah onView() untuk menemukan 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")))));
        }
    }
    

Selanjutnya, kita memiliki penegasan yang akan gagal jika item yang sama dengan "item: 168" berada di tampilan adapter bersama daftar ID.

Untuk contoh selengkapnya, lihat metode testDataItemNotInAdapter() dalam class AdapterViewTest.java di GitHub.

Menggunakan handler kegagalan kustom

Mengganti FailureHandler default di Espresso dengan yang kustom memungkinkan adanya penanganan error tambahan atau yang berbeda, seperti mengambil screenshot atau meneruskan informasi debug tambahan.

Contoh CustomFailureHandlerTest menunjukkan cara mengimplementasikan handler kegagalan kustom:

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

Handler kegagalan ini memunculkan MySpecialException bukan NoMatchingViewException, dan mendelegasikan semua kegagalan lainnya ke DefaultFailureHandler. CustomFailureHandler dapat didaftarkan dengan Espresso dalam metode setUp() pengujian:

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

Untuk informasi selengkapnya, lihat antarmuka FailureHandler dan Espresso.setFailureHandler().

Menargetkan jendela non-default

Android mendukung banyak jendela. Biasanya, fitur ini transparan bagi pengguna dan developer aplikasi, tetapi dalam beberapa kasus beberapa jendela dapat dilihat, seperti saat jendela pelengkapan otomatis digambar di atas jendela aplikasi utama di widget penelusuran. Untuk menyederhanakan banyak hal, secara default Espresso menggunakan heuristik untuk menebak Window mana yang ingin Anda gunakan. Heuristik ini cukup baik dalam tugasnya; tetapi, dalam kasus yang jarang terjadi, Anda harus menentukan jendela mana yang akan ditargetkan oleh interaksi. Anda dapat melakukannya dengan memberikan matcher jendela root atau matcher Root Anda sendiri:

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

Seperti halnya dengan ViewMatchers, kami memberikan satu set RootMatchers yang sebelumnya sudah disediakan. Tentu saja, Anda dapat selalu mengimplementasikan objek Matcher Anda sendiri.

Lihatlah contoh MultipleWindowTest di GitHub.

Header dan footer ditambahkan ke ListViews menggunakan metode addHeaderView() dan addFooterView(). Untuk memastikan Espresso.onData() mengetahui objek data apa yang cocok, pastikan untuk meneruskan nilai objek data preset sebagai parameter kedua ke addHeaderView() dan addFooterView(). Misalnya:

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

Kemudian, Anda dapat menulis matcher untuk footer:

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

Selanjutnya, memuat tampilan dalam pengujian adalah hal yang sepele:

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

        // ...
    }
    

Lihatlah contoh kode selengkapnya, yang ditemukan dalam metode testClickFooter() AdapterViewTest.java di GitHub.