Podstawy espresso

Z tego dokumentu dowiesz się, jak wykonywać typowe zadania automatycznego testowania za pomocą interfejsu Espresso API.

Interfejs Espresso API zachęca autorów testów do myślenia o tym, co użytkownik może zrobić podczas interakcji z aplikacją, czyli lokalizacji elementów interfejsu i interakcji z nimi. Jednocześnie platforma uniemożliwia bezpośredni dostęp do działań i widoków aplikacji, ponieważ utrzymywanie tych obiektów i operowanie na nich poza wątkiem interfejsu jest poważnym źródłem niestabilności testów. W związku z tym w interfejsie Espresso API nie zobaczysz metod takich jak getView() czy getCurrentActivity(). Nadal możesz bezpiecznie pracować na widokach, implementując własne podklasy ViewAction i ViewAssertion.

Komponenty interfejsu API

Główne składniki espresso to:

  • Espresso – punkt wejścia do interakcji z widokami (za pomocą onView() i onData()). Ujawnia też interfejsy API, które nie są powiązane z żadnym widokiem, np. pressBack().
  • ViewMatchers – zbiór obiektów, które implementują interfejs Matcher<? super View>. Możesz przekazać co najmniej 1 z nich do metody onView(), aby zlokalizować widok w bieżącej hierarchii widoków.
  • ViewActions – zbiór obiektów ViewAction, które można przekazać do metody ViewInteraction.perform(), np. click().
  • ViewAssertions – zbiór ViewAssertion obiektów, które można przekazać w metodzie ViewInteraction.check(). W większości przypadków będzie używane asercja dopasowań, która za pomocą dopasowywania widoków pozwala określić stan obecnie wybranego widoku.

Przykład:

Kotlin

// withId(R.id.my_view) is a ViewMatcher
// click() is a ViewAction
// matches(isDisplayed()) is a ViewAssertion
onView(withId(R.id.my_view))
    .perform(click())
    .check(matches(isDisplayed()))

Java

// withId(R.id.my_view) is a ViewMatcher
// click() is a ViewAction
// matches(isDisplayed()) is a ViewAssertion
onView(withId(R.id.my_view))
    .perform(click())
    .check(matches(isDisplayed()));

Znajdowanie widoku

W większości przypadków metoda onView() wykorzystuje dopasowanie skrótu, który ma pasować do jednego i tylko jednego widoku w bieżącej hierarchii widoków. Dopasowania są bardzo skuteczne i będą znane osobom, które używały ich z Mockito lub JUnit. Jeśli nie znasz jeszcze funkcji dopasowywania hamburgerów, najpierw przyjrzyj się tej prezentacji.

Żądany widok często ma unikalny element R.id, a proste dopasowanie withId zawęża wyszukiwanie. Jest jednak wiele uzasadnionych przypadków, w których nie można określić R.id w czasie tworzenia testu. Na przykład konkretny widok może nie mieć elementu R.id lub R.id nie jest unikalny. Może to spowodować, że pisanie zwykłych testów instrumentalnych będzie trudne i skomplikowane, ponieważ zwykły sposób dostępu do widoku danych za pomocą findViewById() nie działa. Może być konieczne uzyskanie dostępu do prywatnych członków aktywności lub fragmentu zawierającego widok albo znalezienie kontenera ze znanym elementem R.id i przejście do jego zawartości w celu odpowiedniego widoku.

Espresso radzi sobie z tym problemem bez problemu, pozwalając Ci zawęzić widok na podstawie istniejących obiektów ViewMatcher lub Twoich własnych.

Aby znaleźć widok według wskaźnika R.id, wystarczy wywołać onView():

Kotlin

onView(withId(R.id.my_view))

Java

onView(withId(R.id.my_view));

Czasami wartości R.id są współdzielone przez wiele widoków. W takim przypadku próba użycia określonego atrybutu R.id umożliwia wyjątek, np. AmbiguousViewMatcherException. Komunikat o wyjątku zawiera tekstową reprezentację bieżącej hierarchii widoków. Możesz ją wyszukać i znaleźć widoki pasujące do nieunikalnych elementów R.id:

java.lang.RuntimeException:
androidx.test.espresso.AmbiguousViewMatcherException
This matcher matches multiple views in the hierarchy: (withId: is <123456789>)

...

+----->SomeView{id=123456789, res-name=plus_one_standard_ann_button,
visibility=VISIBLE, width=523, height=48, has-focus=false, has-focusable=true,
window-focus=true, is-focused=false, is-focusable=false, enabled=true,
selected=false, is-layout-requested=false, text=,
root-is-layout-requested=false, x=0.0, y=625.0, child-count=1}
****MATCHES****
|
+------>OtherView{id=123456789, res-name=plus_one_standard_ann_button,
visibility=VISIBLE, width=523, height=48, has-focus=false, has-focusable=true,
window-focus=true, is-focused=false, is-focusable=true, enabled=true,
selected=false, is-layout-requested=false, text=Hello!,
root-is-layout-requested=false, x=0.0, y=0.0, child-count=1}
****MATCHES****

Porównując różne atrybuty widoków, możesz znaleźć niepowtarzalne właściwości. W przykładzie powyżej jeden z widoków zawiera tekst "Hello!". Aby zawęzić wyszukiwanie, użyj dopasowania kombinacji:

Kotlin

onView(allOf(withId(R.id.my_view), withText("Hello!")))

Java

onView(allOf(withId(R.id.my_view), withText("Hello!")));

Możesz też zrezygnować z cofania dopasowań:

Kotlin

onView(allOf(withId(R.id.my_view), not(withText("Unwanted"))))

Java

onView(allOf(withId(R.id.my_view), not(withText("Unwanted"))));

Informacje o dopasowaniu wyświetleń zapewniane przez Espresso znajdziesz w sekcji ViewMatchers.

co należy wziąć pod uwagę

  • W aplikacji, która działa poprawnie, wszystkie widoki danych, z których użytkownik może wchodzić w interakcje, powinny zawierać opis lub opis treści. Więcej informacji znajdziesz w sekcji Zwiększanie dostępności aplikacji. Jeśli nie możesz zawęzić wyszukiwania przy użyciu withText() lub withContentDescription(), potraktuj to jako błąd ułatwień dostępu.
  • Aby znaleźć ten widok, użyj najmniej opisowej funkcji dopasowania. Nie określaj zbyt wielu terminów, ponieważ zmusi to platformę do wykonania większej ilości zadań, niż jest to konieczne. Jeśli np. widok danych można jednoznacznie zidentyfikować na podstawie jego tekstu, nie musisz określać, że można go przypisać również z poziomu TextView. W przypadku wielu widoków wystarczy ich R.id.
  • Jeśli widok docelowy znajduje się w elemencie AdapterView, np. ListView, GridView lub Spinner, metoda onView() może nie działać. W takich przypadkach należy zamiast tego użyć elementu onData().

Wykonywanie działania na widoku

Gdy znajdziesz odpowiednie dopasowanie dla widoku docelowego, możesz wykonać w nim wystąpienia ViewAction, korzystając z metody wykonywania.

Aby np. kliknąć ten widok:

Kotlin

onView(...).perform(click())

Java

onView(...).perform(click());

W ramach jednego wywołania wykonawczego możesz wykonać więcej niż jedno działanie:

Kotlin

onView(...).perform(typeText("Hello"), click())

Java

onView(...).perform(typeText("Hello"), click());

Jeśli widok, z którego pracujesz, znajduje się wewnątrz elementu ScrollView (w pionie lub w poziomie), zastanów się nad poprzedzającymi go działaniami, które wymagają wyświetlenia widoku, np. click() i typeText() – w elemencie scrollTo(). Dzięki temu przed przejściem do kolejnego działania widok zostanie wyświetlony:

Kotlin

onView(...).perform(scrollTo(), click())

Java

onView(...).perform(scrollTo(), click());

Listę działań wyświetlania zapewnianych przez Espresso znajdziesz tutaj: ViewActions.

Sprawdzanie asercji widoku

Asercje można stosować do obecnie wybranego widoku za pomocą metody check(). Najczęściej używane asercja to matches(). Do określania stanu obecnie wybranego widoku służy obiekt ViewMatcher.

Aby na przykład sprawdzić, czy widok zawiera tekst "Hello!":

Kotlin

onView(...).check(matches(withText("Hello!")))

Java

onView(...).check(matches(withText("Hello!")));

Jeśli chcesz potwierdzić, że materiał "Hello!" zawiera treść wyświetlenia, uznajemy to za niewłaściwą praktykę:

Kotlin

// Don't use assertions like withText inside onView.
onView(allOf(withId(...), withText("Hello!"))).check(matches(isDisplayed()))

Java

// Don't use assertions like withText inside onView.
onView(allOf(withId(...), withText("Hello!"))).check(matches(isDisplayed()));

Jeśli z drugiej strony chcesz potwierdzić, że widok z tekstem "Hello!" jest widoczny – na przykład po zmianie flagi widoczności widoków – kod jest prawidłowy.

Wyświetl prosty test asercji

W tym przykładzie SimpleActivity zawiera Button i TextView. Po kliknięciu przycisku zawartość elementu TextView zmieni się na "Hello Espresso!".

Oto, jak możesz to przetestować z espresso:

Kliknij przycisk

Pierwszym krokiem jest znalezienie usługi, która pomaga znaleźć przycisk. Zgodnie z oczekiwaniami przycisk w elemencie SimpleActivity ma unikalny element R.id.

Kotlin

onView(withId(R.id.button_simple))

Java

onView(withId(R.id.button_simple));

Teraz wykonaj kliknięcie:

Kotlin

onView(withId(R.id.button_simple)).perform(click())

Java

onView(withId(R.id.button_simple)).perform(click());

Sprawdzanie tekstu TextView

TextView z tekstem do weryfikacji ma też unikalny identyfikator R.id:

Kotlin

onView(withId(R.id.text_simple))

Java

onView(withId(R.id.text_simple));

Teraz sprawdź tekst treści:

Kotlin

onView(withId(R.id.text_simple)).check(matches(withText("Hello Espresso!")))

Java

onView(withId(R.id.text_simple)).check(matches(withText("Hello Espresso!")));

Sprawdź wczytywanie danych w widokach adaptera

AdapterView to specjalny typ widżetu, który dynamicznie wczytuje dane z adaptera. Najczęstszym przykładem właściwości AdapterView jest ListView. W przeciwieństwie do widżetów statycznych, takich jak LinearLayout, w bieżącej hierarchii widoków może być wczytywana tylko część widżetów podrzędnych AdapterView. Proste wyszukiwanie onView() nie znajdzie widoków, które nie są aktualnie wczytane.

Espresso zapewnia osobny punkt wejścia onData(), który umożliwia pierwsze wczytanie odpowiedniego elementu adaptera, aktywując go przed uruchomieniem operacji na nim lub jego elementach podrzędnych.

Ostrzeżenie: niestandardowe implementacje AdapterView mogą mieć problemy z metodą onData(), jeśli naruszają umowy dziedziczenia, a zwłaszcza interfejs API getItem(). W takich przypadkach najlepszym rozwiązaniem jest refaktoryzacja kodu aplikacji. Jeśli nie możesz tego zrobić, możesz wdrożyć pasujący niestandardowy element AdapterViewProtocol. Więcej informacji znajdziesz w domyślnej klasie AdapterViewProtocols udostępnianej przez Espresso.

Prosty test widoku adaptera

Ten prosty test pokazuje, jak używać onData(). SimpleActivity zawiera Spinner z kilkoma elementami reprezentującymi rodzaje napojów kawowych. Po wybraniu elementu pojawia się TextView, który zmienia się w "One %s a day!", gdzie %s oznacza wybrany element.

Celem tego testu jest otwarcie Spinner, wybranie konkretnego elementu i sprawdzenie, czy TextView go zawiera. Klasa Spinner bazuje na AdapterView, dlatego do dopasowywania elementu zalecamy użycie onData() zamiast onView().

Otwórz wybór elementu

Kotlin

onView(withId(R.id.spinner_simple)).perform(click())

Java

onView(withId(R.id.spinner_simple)).perform(click());

Zaznaczenie elementu

W przypadku wybranego elementu Spinner tworzy ListView z jego zawartością. Ten widok może być bardzo długi, a element może nie być uwzględniany w hierarchii widoków. Korzystając z elementu onData(), wymuszamy umieszczenie wybranego elementu w hierarchii widoku. Elementy w polu Spinner to ciągi tekstowe, więc chcemy dopasować element, który ma równy ciąg "Americano":

Kotlin

onData(allOf(`is`(instanceOf(String::class.java)),
        `is`("Americano"))).perform(click())

Java

onData(allOf(is(instanceOf(String.class)), is("Americano"))).perform(click());

Sprawdzanie, czy tekst jest poprawny

Kotlin

onView(withId(R.id.spinnertext_simple))
    .check(matches(withText(containsString("Americano"))))

Java

onView(withId(R.id.spinnertext_simple))
    .check(matches(withText(containsString("Americano"))));

Debugowanie

Espresso dostarcza przydatne informacje na potrzeby debugowania, jeśli test się nie powiedzie:

Logowanie

Espresso zapisuje wszystkie działania związane z wyświetlaniem w logcat. Na przykład:

ViewInteraction: Performing 'single click' action on view with text: Espresso

Wyświetl hierarchię

W przypadku niepowodzenia onView() Espresso drukuje hierarchię widoków w komunikacie o wyjątku.

  • Jeśli onView() nie znajdzie widoku docelowego, wysyłany jest NoMatchingViewException. Możesz zbadać hierarchię widoków danych w ciągu wyjątków, aby sprawdzić, dlaczego dopasowanie nie pasowało do żadnego widoku.
  • Jeśli onView() znajdzie wiele widoków danych odpowiadających danemu dopasowaniu, wywoływany jest element AmbiguousViewMatcherException. Hierarchia widoków jest wydrukowana, a wszystkie pasujące widoki są oznaczone etykietą MATCHES:
java.lang.RuntimeException:
androidx.test.espresso.AmbiguousViewMatcherException
This matcher matches multiple views in the hierarchy: (withId: is <123456789>)

...

+----->SomeView{id=123456789, res-name=plus_one_standard_ann_button,
visibility=VISIBLE, width=523, height=48, has-focus=false, has-focusable=true,
window-focus=true, is-focused=false, is-focusable=false, enabled=true,
selected=false, is-layout-requested=false, text=,
root-is-layout-requested=false, x=0.0, y=625.0, child-count=1}
****MATCHES****
|
+------>OtherView{id=123456789, res-name=plus_one_standard_ann_button,
visibility=VISIBLE, width=523, height=48, has-focus=false, has-focusable=true,
window-focus=true, is-focused=false, is-focusable=true, enabled=true,
selected=false, is-layout-requested=false, text=Hello!,
root-is-layout-requested=false, x=0.0, y=0.0, child-count=1}
****MATCHES****

Gdy zmagasz się ze skomplikowaną hierarchią widoków lub nieoczekiwanym działaniem widżetów, zawsze możesz użyć przeglądarki hierarchii w Android Studio, aby uzyskać wyjaśnienie.

Ostrzeżenia dotyczące widoku adaptera

Espresso ostrzega użytkowników o AdapterView widżetach. Gdy operacja onView() zgłasza widżety NoMatchingViewException, a w hierarchii widoków znajdują się widżety AdapterView, najczęstszym rozwiązaniem jest użycie metody onData(). Komunikat o wyjątku będzie zawierał ostrzeżenie z listą widoków adaptera. Możesz użyć tych informacji do wywołania onData(), aby wczytać widok docelowy.

Dodatkowe materiały

Więcej informacji o używaniu Espresso w testach na Androidzie znajdziesz w tych materiałach.

Próbki