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()
ionData()
). 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 metodyonView()
, aby zlokalizować widok w bieżącej hierarchii widoków. - ViewActions – zbiór obiektów
ViewAction
, które można przekazać do metodyViewInteraction.perform()
, np.click()
. - ViewAssertions – zbiór
ViewAssertion
obiektów, które można przekazać w metodzieViewInteraction.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()
lubwithContentDescription()
, 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 ichR.id
. - Jeśli widok docelowy znajduje się w elemencie
AdapterView
, np.ListView
,GridView
lubSpinner
, metodaonView()
może nie działać. W takich przypadkach należy zamiast tego użyć elementuonData()
.
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 jestNoMatchingViewException
. 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 elementAmbiguousViewMatcherException
. 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
- CustomMatcherSample:
pokazuje, jak rozszerzyć Espresso, aby pasowało do właściwości wskazówki obiektu
EditText
. - RecyclerViewSample:
działania
RecyclerView
dla Espresso. - (więcej...)