W tym dokumencie opisujemy, jak skonfigurować różne często używane testy espresso.
Dopasowywanie widoku obok innego widoku
Układ może zawierać określone widoki, które nie są niepowtarzalne. Na przykład przycisk cyklicznego połączenia w tabeli kontaktów może mieć taki sam element R.id
, zawierać ten sam tekst i mieć te same właściwości co inne przyciski wywołania w hierarchii widoków.
Na przykład w tym ćwiczeniu widok z tekstem "7"
powtarza się w wielu wierszach:
Nieunikalny widok jest często powiązany z jakąś unikalną etykietą, która znajduje się obok niego, np. nazwą kontaktu obok przycisku połączenia. W takim przypadku możesz użyć dopasowania hasSibling()
, aby zawęzić wybór:
Kotlin
onView(allOf(withText("7"), hasSibling(withText("item: 0")))) .perform(click())
Java
onView(allOf(withText("7"), hasSibling(withText("item: 0")))) .perform(click());
Dopasowywanie widoku na pasku działań
ActionBarTestActivity
ma 2 paski działań: zwykły pasek działań i pasek działań kontekstowych, który możesz utworzyć w menu opcji. Oba paski działań zawierają 1 element, który jest zawsze widoczny, i 2 elementy, które są widoczne tylko w rozszerzonym menu. Kliknięcie elementu powoduje zmianę pola TextView na zawartość klikniętego elementu.
Dopasowywanie widocznych ikon na obu paskach działań jest proste, co pokazuje ten fragment kodu:
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"))); }
Kod wygląda tak samo na pasku działań kontekstowych:
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"))); }
Klikanie elementów w rozszerzonym menu jest trochę trudniejsze na normalnym pasku działań, ponieważ niektóre urządzenia mają sprzętowy przycisk rozszerzonego menu, który otwiera przewijane elementy w menu opcji, a na niektórych urządzeniach jest przycisk oprogramowania, którego menu rozszerzone się otwiera. Na szczęście Espresso robi to za nas.
Na normalnym pasku działań:
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"))); }
Tak to wygląda na urządzeniach ze sprzętowym przyciskiem rozszerzonego menu:
Ponowne uruchamianie paska działań kontekstowych jest bardzo proste:
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"))); } }
Aby zobaczyć pełny kod tych przykładów, wyświetl na GitHubie przykładowy kod ActionBarTest.java
.
Twierdzenie, że widok nie jest wyświetlany
Po wykonaniu serii działań warto potwierdzić stan testowanego interfejsu użytkownika. Czasem może to być niekorzystne,
np. gdy coś się nie dzieje. Pamiętaj, że możesz przekształcić dowolne dopasowanie widoku hamcrest w ViewAssertion
, używając funkcji ViewAssertions.matches()
.
W poniższym przykładzie wybieramy dopasowanie isDisplayed()
i odwracamy je, używając standardowego dopasowania 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())));
Powyższe podejście działa, jeśli widok jest nadal częścią hierarchii. Jeśli nie, otrzymasz NoMatchingViewException
. Musisz użyć aplikacji ViewAssertions.doesNotExist()
.
Twierdzenie o braku widoku
Jeśli widok danych zniknie z hierarchii widoków – co może się zdarzyć, gdy działanie spowodowało przejście do innej aktywności – użyj 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());
Twierdzić, że elementu danych nie ma w adapterze.
Aby udowodnić, że określony element danych nie znajduje się w elemencie AdapterView
, musisz zrobić to trochę inaczej. Musimy znaleźć interesującą Cię usługę AdapterView
i przeanalizować dane, które się w niej znajdują. Nie musimy używać usługi onData()
.
Zamiast tego używamy onView()
, aby znaleźć AdapterView
, a następnie używamy innego dopasowania, aby pracować z danymi w widoku.
Najpierw dopasowanie:
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; } }; }
Następnie wystarczy onView()
, aby znaleźć: 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"))))); } }
Mamy też asercję, która nie powiedzie się, jeśli element o wartości „item: 168” występuje w widoku adaptera z listą identyfikatorów.
Pełny przykład znajdziesz w metodzie testDataItemNotInAdapter()
w klasie AdapterViewTest.java
na GitHubie.
Używanie niestandardowego modułu obsługi błędów
Zastąpienie domyślnego FailureHandler
w Espresso adresem niestandardowym pozwala na dodatkową lub inną obsługę błędów, takich jak robienie zrzutu ekranu lub przekazywanie dodatkowych informacji na potrzeby debugowania.
Przykład CustomFailureHandlerTest
pokazuje, jak wdrożyć niestandardowy moduł obsługi awarii:
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); } } }
Ten moduł obsługi błędów zgłasza MySpecialException
zamiast NoMatchingViewException
i przekazuje wszystkie inne błędy do DefaultFailureHandler
. CustomFailureHandler
można zarejestrować w Espresso w metodzie setUp()
testu:
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())); }
Więcej informacji znajdziesz w interfejsie FailureHandler
i
Espresso.setFailureHandler()
.
Kierowanie na okna inne niż domyślne
Android obsługuje wiele okien. Zwykle jest to widoczne dla użytkowników i dewelopera aplikacji, ale w niektórych przypadkach widocznych jest wiele okien, na przykład po przeciągnięciu okna autouzupełniania nad głównym oknem aplikacji w widżecie wyszukiwania. Aby uprościć sprawę, domyślnie Espresso korzysta z metody heurystycznej do odgadnięcia, z którym elementem Window
chcesz wejść w interakcję. Taka heurystyka prawie zawsze jest wystarczająca, ale w rzadkich przypadkach trzeba określić okno, na które ma być kierowana interakcja. Możesz to zrobić, podając własny mechanizm dopasowywania okna głównego lub dopasowanie 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());
Tak jak w przypadku ViewMatchers
, udostępniamy zestaw wstępnie przygotowanych RootMatchers
.
Oczywiście zawsze możesz zaimplementować własny obiekt Matcher
.
Zapoznaj się z przykładowym testem MultipleWindowTest na GitHubie.
Dopasowanie nagłówka lub stopki w widoku listy
Nagłówki i stopki są dodawane do pliku ListViews
za pomocą metod addHeaderView()
i addFooterView()
. Aby mieć pewność, że funkcja Espresso.onData()
wie, do którego obiektu danych ma pasować, przekaż gotową wartość obiektu danych jako drugi parametr do obiektów addHeaderView()
i addFooterView()
. Na przykład:
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);
Następnie możesz zastosować dopasowanie stopki:
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)); }
Wczytywanie widoku w teście jest proste:
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()); // ... }
Zapoznaj się z pełnym przykładem kodu dostępnym w metodzie testClickFooter()
w AdapterViewTest.java
na GitHubie.