Espressorezepte

In diesem Dokument wird die Einrichtung einer Vielzahl gängiger Espresso-Tests beschrieben.

Eine Ansicht mit einer anderen Ansicht abgleichen

Ein Layout kann bestimmte Ansichten enthalten, die an sich nicht eindeutig sind. Beispielsweise kann eine Schaltfläche für wiederholte Anrufe in einer Kontakttabelle dieselbe R.id, denselben Text und dieselben Eigenschaften wie andere Anrufschaltflächen in der Ansichtshierarchie haben.

In dieser Aktivität wird die Ansicht mit dem Text "7" beispielsweise in mehreren Zeilen wiederholt:

Eine Listenaktivität, bei der 3 Kopien desselben Ansichtselements in einer Liste mit drei Elementen angezeigt werden

Häufig wird die nicht eindeutige Ansicht mit einem eindeutigen Label kombiniert, das sich daneben befindet, z. B. dem Namen des Kontakts neben der Anrufschaltfläche. In diesem Fall können Sie die Auswahl mit dem hasSibling()-Matcher eingrenzen:

Kotlin

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

Java

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

Eine Ansicht innerhalb einer Aktionsleiste anzeigen lassen

Die ActionBarTestActivity hat zwei verschiedene Aktionsleisten: eine normale und eine kontextbezogene Aktionsleiste, die über ein Optionsmenü erstellt wird. Beide Aktionsleisten enthalten ein Element, das immer sichtbar ist, und zwei Elemente, die nur im Dreipunkt-Menü sichtbar sind. Wenn ein Element angeklickt wird, wird ein TextView zum Inhalt des angeklickten Elements geändert.

Der Abgleich sichtbarer Symbole auf beiden Aktionsleisten ist einfach, wie im folgenden Code-Snippet gezeigt:

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

Die Schaltfläche „Speichern“ befindet sich in der Aktionsleiste oben in der Aktivität

Der Code für die kontextbezogene Aktionsleiste sieht identisch aus:

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

Die Schaltfläche zum Sperren befindet sich in der Aktionsleiste oben in der Aktivität

Das Klicken auf Elemente im Dreipunkt-Menü ist für die normale Aktionsleiste etwas komplizierter, da einige Geräte eine Schaltfläche für das Dreipunkt-Menü haben, mit der die überlaufenden Elemente in einem Optionsmenü geöffnet werden. Auf einigen Geräten gibt es eine Schaltfläche für das Dreipunkt-Menü, mit der ein normales Dreipunkt-Menü geöffnet wird. Glücklicherweise übernimmt Espresso das für uns.

Für die normale Aktionsleiste:

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

Die Schaltfläche für das Dreipunkt-Menü ist sichtbar und unterhalb der Aktionsleiste am oberen Bildschirmrand wird eine Liste angezeigt

Auf Geräten mit einer Schaltfläche für das Dreipunkt-Menü der Hardware sieht das so aus:

Es gibt keine Dreipunkt-Menüschaltfläche und am unteren Bildschirmrand wird eine Liste angezeigt

Für die kontextbezogene Aktionsleiste ist das wieder ganz einfach:

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

Die Schaltfläche für das Dreipunkt-Menü wird in der Aktionsleiste angezeigt und die Liste der Optionen wird unter der Aktionsleiste oben auf dem Bildschirm angezeigt.

Den vollständigen Code für diese Beispiele finden Sie im Beispiel ActionBarTest.java auf GitHub.

Erklären, dass eine Ansicht nicht angezeigt wird

Nachdem Sie eine Reihe von Aktionen ausgeführt haben, sollten Sie auf jeden Fall den Status der zu testenden UI bestätigen. Manchmal ist dies ein negativer Fall, z. B. wenn etwas nicht passiert. Denken Sie daran, dass Sie jeden Hamcrest View-Matcher mit ViewAssertions.matches() in einen ViewAssertion umwandeln können.

Im folgenden Beispiel nehmen wir den isDisplayed()-Matcher und kehren ihn mit dem Standard-Matcher not() um:

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

Der obige Ansatz funktioniert, wenn die Ansicht immer noch Teil der Hierarchie ist. Ist dies nicht der Fall, erhalten Sie ein NoMatchingViewException und müssen ViewAssertions.doesNotExist() verwenden.

Erklären, dass keine Ansicht vorhanden ist

Wenn die Ansicht aus der Ansichtshierarchie entfernt wird – was passieren kann, wenn eine Aktion einen Übergang zu einer anderen Aktivität verursacht –, sollten Sie ViewAssertions.doesNotExist() verwenden:

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

Feststellen, dass sich ein Datenelement nicht in einem Adapter befindet

Um nachzuweisen, dass ein bestimmtes Datenelement nicht in einem AdapterView enthalten ist, müssen Sie die Dinge etwas anders machen. Wir müssen die für uns interessante AdapterView finden und die darin enthaltenen Daten abfragen. Wir müssen onData() nicht verwenden. Stattdessen verwenden wir onView(), um AdapterView zu finden, und dann einen anderen Abgleich, um die Daten in der Ansicht zu bearbeiten.

Zuerst der Matcher:

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

Dann benötigen wir nur onView(), um AdapterView zu finden:

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

Es gibt auch eine Assertion, die fehlschlägt, wenn in einer Adapteransicht mit der ID-Liste ein Element vorhanden ist, das gleich „item: 168“ ist.

Das vollständige Beispiel finden Sie in der Methode testDataItemNotInAdapter() in der Klasse AdapterViewTest.java auf GitHub.

Benutzerdefinierten Fehler-Handler verwenden

Wenn Sie die standardmäßige FailureHandler in Espresso durch eine benutzerdefinierte ersetzen, können Sie zusätzliche oder andere Fehlerbehandlung vornehmen, z. B. einen Screenshot erstellen oder zusätzliche Informationen zur Fehlerbehebung weitergeben.

Das Beispiel CustomFailureHandlerTest zeigt, wie ein benutzerdefinierter Fehler-Handler implementiert wird:

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

Dieser Fehler-Handler gibt MySpecialException anstelle von NoMatchingViewException aus und delegiert alle anderen Fehler an DefaultFailureHandler. Die CustomFailureHandler kann in der setUp()-Methode des Tests bei Espresso registriert werden:

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

Weitere Informationen finden Sie auf der FailureHandler-Oberfläche und in Espresso.setFailureHandler().

Nicht standardmäßige Fenster als Ziel

Android unterstützt mehrere Fenster. Normalerweise ist dies für Nutzer und App-Entwickler transparent, in bestimmten Fällen sind jedoch mehrere Fenster sichtbar, z. B. wenn im Such-Widget ein Fenster mit automatischer Vervollständigung über das Hauptanwendungsfenster gezogen wird. Der Einfachheit halber verwendet Espresso standardmäßig eine Heuristik, um zu ermitteln, mit welcher Window du interagieren möchtest. Diese Heuristik ist fast immer gut genug. In seltenen Fällen müssen Sie jedoch angeben, auf welches Fenster eine Interaktion abzielen soll. Dazu können Sie Ihren eigenen Root-Window-Matcher oder einen Root-Matcher bereitstellen:

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

Wie bei ViewMatchers stellen wir eine Reihe vorkonfigurierter RootMatchers bereit. Natürlich können Sie jederzeit ein eigenes Matcher-Objekt implementieren.

Sehen Sie sich das MultipleWindowTest-Beispiel auf GitHub an.

Kopf- und Fußzeilen werden mit den Methoden addHeaderView() und addFooterView() zu ListViews hinzugefügt. Damit Espresso.onData() weiß, welches Datenobjekt abgeglichen werden soll, müssen Sie einen voreingestellten Datenobjektwert als zweiten Parameter an addHeaderView() und addFooterView() übergeben. Beispiele:

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

Anschließend können Sie einen Abgleich für die Fußzeile schreiben:

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

Und das Laden der Ansicht in einem Test ist ganz einfach:

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

    // ...
}

Sehen Sie sich das vollständige Codebeispiel an, das in der Methode testClickFooter() von AdapterViewTest.java auf GitHub enthalten ist.