W tym dokumencie opisujemy, jak skonfigurować różne popularne testy Espresso.
Dopasowanie widoku do innego widoku
Układ może zawierać określone widoki, które same z siebie nie są unikalne. Dla:
Na przykład przycisk powtarzania połączenia w tabeli kontaktów może mieć taki sam
R.id
, zawierają ten sam tekst i mają te same właściwości co inne wywołanie
w hierarchii widoków.
Na przykład w tej aktywności widok z tekstem "7"
powtarza się w wielu miejscach
wiersze:
Nieunikalny widok jest często powiązany z unikalną etykietą umieszczoną
obok niej, np. nazwę kontaktu obok przycisku połączenia. W tym przypadku
za pomocą dopasowania hasSibling()
możesz 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());
Dopasowanie do widoku w pasku działań
ActionBarTestActivity
ma 2 różne paski działań: normalny
paska działań i paska działań kontekstowych utworzonego z menu opcji. Obie opcje
Pasek działań zawiera 1 zawsze widoczny element i 2 elementy
widoczne w rozszerzonym menu. Kliknięcie elementu powoduje zmianę obiektu TextView na
treść klikniętego elementu.
Jak widać, dopasowanie widocznych ikon na obu paskach działań jest proste. w tym fragmencie 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 identycznie w przypadku paska 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 nieco trudniejsze do wykonania w zwykłym trybie. bo niektóre urządzenia mają sprzętowy przycisk rozszerzonego menu, który otwiera dodatkowe pozycje w menu opcji, a na niektórych urządzeniach występuje nadmiar zawartości oprogramowania przycisk menu, który otwiera normalne rozszerzone menu. Na szczęście Espresso radzi sobie dla nas.
W przypadku zwykłego paska 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"))); }
Na urządzeniach z przyciskiem rozszerzonego menu sprzętowego ta strona wygląda tak:
W przypadku paska działań kontekstowych jest to 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
ActionBarTest.java
– przykład w GitHubie.
Potwierdzanie, że widok nie jest wyświetlany
Po wykonaniu serii działań warto potwierdzić, że
w testowanym stanie interfejsu. Czasami może to być negatywne, np.
że coś się nie dzieje. Pamiętaj, że w dowolnym widoku możesz włączyć widok hamcrest,
dopasowania do funkcji ViewAssertion
przy użyciu funkcji ViewAssertions.matches()
.
W przykładzie poniżej stosujemy dopasowanie isDisplayed()
i odwracamy je za pomocą funkcji
standardowe dopasowanie 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 sprawdza się, jeśli widok nadal jest częścią hierarchii. Jeśli tak
nie, otrzymasz NoMatchingViewException
i konieczne będzie użycie
ViewAssertions.doesNotExist()
Potwierdzanie braku widoku
Jeśli widok danych został usunięty z hierarchii widoków – co może się zdarzyć, gdy
spowodowała przejście do innej aktywności.
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());
Trzeba twierdzić, że element danych nie znajduje się w adapterze
Aby udowodnić, że określony element danych nie znajduje się w lokalizacji AdapterView
, musisz wykonać
wszystko wygląda nieco inaczej. Musimy znaleźć AdapterView
, który nas interesuje
i przeanalizuje przechowywane dane. Nie musimy używać funkcji onData()
.
Zamiast tego używamy wyrażenia onView()
, aby znaleźć AdapterView
, a następnie innej
na podstawie danych 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; } }; }
Potem wystarczy już tylko 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ż potwierdzenie, które nie powiedzie się, jeśli element równy „item: 168”. znajduje się w widoku adaptacyjnym z listą identyfikatorów.
Pełną próbkę znajdziesz w metodzie testDataItemNotInAdapter()
w sekcji
AdapterViewTest.java
.
znajdziesz na GitHubie.
Użyj niestandardowego modułu obsługi błędów
Zastępowanie domyślnej wartości FailureHandler
w Espresso na niestandardową umożliwia:
dodatkowe lub inne sposoby obsługi błędów, np. robienie zrzutu ekranu,
wraz z dodatkowymi informacjami na temat debugowania.
Przykład żądania CustomFailureHandlerTest
pokazuje, jak wdrożyć niestandardową regułę
moduł obsługi błędów:
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 zwraca MySpecialException
zamiast
NoMatchingViewException
i przekazuje wszystkie pozostałe błędy do
DefaultFailureHandler
Urządzenie CustomFailureHandler
można zarejestrować w usłudze
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:
FailureHandler
i
Espresso.setFailureHandler()
Kieruj reklamy na okna inne niż domyślne
Android obsługuje wiele okien. Zwykle te informacje są przejrzyste dla użytkowników.
i deweloperem aplikacji, ale w niektórych przypadkach widocznych jest wiele okien,
np. okno autouzupełniania
wyświetlane na głównym oknie aplikacji
w widżecie wyszukiwania. Aby uprościć ten proces, Espresso domyślnie korzysta z algorytmu heurystycznego,
odgadnij, z którą usługą Window
chcesz wejść w interakcję. Ta heurystyka jest prawie
zawsze wystarczająco dobre, ale w rzadkich przypadkach musisz określić, który okres
na daną interakcję. Możesz to zrobić, dodając własne okno główne
dopasowanie 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
dostępny jest zestaw
RootMatchers
Zawsze możesz też zaimplementować własny obiekt Matcher
.
Zapoznaj się z narzędziem MultipleWindowTest fragment w GitHubie.
Dopasowanie nagłówka lub stopki w widoku listy
Nagłówki i stopki są dodawane do ListViews
za pomocą elementów addHeaderView()
i
addFooterView()
metod. Aby mieć pewność, że usługa Espresso.onData()
wie, który obiekt danych
aby dopasować, przekaż gotową wartość obiektu danych jako drugi parametr.
do: 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 napisać odpowiednik 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)); }
A wczytanie widoku podczas testu 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()); // ... }
Spójrz na pełny przykładowy kod znaleziony w metodzie testClickFooter()
AdapterViewTest.java
.
w GitHubie.