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:
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"))); }
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"))); }
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"))); }
Auf Geräten mit einer Schaltfläche für das Dreipunkt-Menü der Hardware sieht das so aus:
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"))); } }
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- oder Fußzeilen in einer Listenansicht abgleichen
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.