Nozioni di base su espresso

Questo documento spiega come completare le attività di test automatiche più comuni utilizzando l'API Espresso.

L'API Espresso incoraggia gli autori di test a pensare in termini di cosa un utente potrebbe fare mentre interagisce con l'applicazione, individuando gli elementi dell'interfaccia utente e interagendo con loro. Allo stesso tempo, il framework impedisce l'accesso diretto alle attività e alle visualizzazioni dell'applicazione perché mantenere questi oggetti e utilizzarli dal thread dell'interfaccia utente è una delle principali fonti di problemi di test. Pertanto, non vedrai metodi come getView() e getCurrentActivity() nell'API Espresso. Puoi comunque eseguire in sicurezza le viste implementando le tue sottoclassi ViewAction e ViewAssertion.

Componenti API

I componenti principali di Espresso sono i seguenti:

  • Espresso: punto di ingresso per le interazioni con le viste (tramite onView() e onData()). Espone anche le API che non sono necessariamente legate a una vista, ad esempio pressBack().
  • ViewMatchers: una raccolta di oggetti che implementano l'interfaccia di Matcher<? super View>. Puoi passare uno o più di questi elementi al metodo onView() per individuare una vista all'interno della gerarchia di visualizzazione corrente.
  • ViewActions: una raccolta di ViewAction oggetti che possono essere trasmessi al metodo ViewInteraction.perform(), ad esempio click().
  • ViewAssertions: una raccolta di ViewAssertion oggetti che possono essere passati al metodo ViewInteraction.check(). La maggior parte delle volte utilizzerai l'asserzione Corrispondenze, che utilizza un matcher Visualizza per rivendicare lo stato della vista attualmente selezionata.

Esempio:

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()));

Trovare una visualizzazione

Nella stragrande maggioranza dei casi, il metodo onView() prende in considerazione un matcher hamcrest che corrisponde a una sola visualizzazione all'interno della gerarchia di visualizzazioni corrente. I matcher sono potenti e saranno familiari a chi li ha utilizzati con Mockito o JUnit. Se non hai dimestichezza con gli abbinamenti hamcrest, ti suggeriamo di iniziare dando un'occhiata veloce a questa presentazione.

Spesso la visualizzazione desiderata ha un valore R.id univoco e un semplice matcher withId restringe la ricerca della visualizzazione. Tuttavia, in molti casi legittimi non è possibile determinare R.id durante lo sviluppo del test. Ad esempio, la vista specifica potrebbe non avere un R.id o R.id non è univoco. Ciò può rendere i normali test di strumentazione fragili e complicati da scrivere, poiché il modo normale per accedere alla vista, con findViewById(), non funziona. Di conseguenza, potresti dover accedere ai membri privati dell'attività o del frammento che contengono la vista o trovare un container con un R.id noto e accedere ai suoi contenuti per la visualizzazione specifica.

Espresso gestisce questo problema in modo pulito, consentendoti di restringere la visualizzazione utilizzando oggetti ViewMatcher esistenti o personalizzati.

Trovare una vista in base al suo R.id è semplice come chiamare onView():

Kotlin

onView(withId(R.id.my_view))

Java

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

A volte, i valori R.id sono condivisi tra più viste. In questo caso, il tentativo di utilizzare un determinato R.id genera un'eccezione, ad esempio AmbiguousViewMatcherException. Il messaggio di eccezione fornisce una rappresentazione testuale della gerarchia corrente delle viste, in cui puoi cercare e trovare le viste che corrispondono al valore R.id non univoco:

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****

Esaminando i vari attributi delle viste, potresti trovare proprietà identificabili in modo univoco. Nell'esempio precedente, una delle visualizzazioni ha il testo "Hello!". Puoi utilizzare questa opzione per restringere la ricerca utilizzando associatori di combinazioni:

Kotlin

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

Java

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

Puoi anche scegliere di non invertire nessuno dei matcher:

Kotlin

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

Java

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

Consulta la pagina ViewMatchers per i matcher delle visualizzazioni forniti da Espresso.

considerazioni

  • In un'applicazione corretta, tutte le viste con cui un utente può interagire devono contenere testo descrittivo o avere una descrizione dei contenuti. Consulta Rendere più accessibili le app per ulteriori dettagli. Se non riesci a restringere una ricerca utilizzando withText() o withContentDescription(), valuta la possibilità di considerarla come un bug di accessibilità.
  • Utilizza il matcher meno descrittivo che trova la visualizzazione che stai cercando. Non specificare troppo, in quanto questo costringerà il framework a svolgere più lavoro del necessario. Ad esempio, se una vista è identificabile in modo univoco dal suo testo, non è necessario specificare che può essere assegnata anche da TextView. Per molte visualizzazioni, il valore R.id della visualizzazione dovrebbe essere sufficiente.
  • Se la vista di destinazione si trova all'interno di un elemento AdapterView, come ListView, GridView o Spinner, il metodo onView() potrebbe non funzionare. In questi casi, devi utilizzare invece onData().

Eseguire un'azione su una vista

Dopo aver trovato un matcher adatto per la vista di destinazione, è possibile eseguire istanze di ViewAction su di esso utilizzando il metodo perform.

Ad esempio, per fare clic sulla visualizzazione:

Kotlin

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

Java

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

Puoi eseguire più di un'azione con una chiamata Esegui:

Kotlin

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

Java

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

Se la vista su cui stai lavorando si trova all'interno di un elemento ScrollView (verticale o orizzontale), valuta le azioni precedenti che richiedono la visualizzazione della vista, ad esempio click() e typeText(), con scrollTo(). In questo modo viene garantita la visualizzazione della visualizzazione prima di procedere con l'altra azione:

Kotlin

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

Java

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

Consulta la sezione ViewActions per le azioni di visualizzazione fornite da Espresso.

Seleziona Visualizza asserzioni

Le asserzioni possono essere applicate alla vista attualmente selezionata con il metodo check(). L'asserzione più utilizzata è matches(). Utilizza un oggetto ViewMatcher per dichiarare lo stato della vista attualmente selezionata.

Ad esempio, per verificare che una vista abbia il testo "Hello!":

Kotlin

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

Java

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

Per affermare che "Hello!" è il contenuto della visualizzazione, la seguente è considerata una prassi scorretta:

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()));

D'altra parte, se vuoi dichiarare che una vista con il testo "Hello!" è visibile, ad esempio dopo una modifica del flag di visibilità delle viste, il codice è corretto.

Visualizza il test semplice dell'asserzione

In questo esempio, SimpleActivity contiene Button e TextView. Quando fai clic sul pulsante, il contenuto di TextView diventa "Hello Espresso!".

Ecco come eseguire un test con Espresso:

Fai clic sul pulsante

Il primo passaggio è cercare una proprietà che aiuti a trovare il pulsante. Il pulsante in SimpleActivity ha un valore R.id univoco, come previsto.

Kotlin

onView(withId(R.id.button_simple))

Java

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

A questo punto, per eseguire il clic:

Kotlin

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

Java

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

Verificare il testo TextView

Anche il TextView con il testo da verificare ha un R.id univoco:

Kotlin

onView(withId(R.id.text_simple))

Java

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

Ora, per verificare il testo dei contenuti:

Kotlin

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

Java

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

Controlla il caricamento dei dati nelle visualizzazioni adattatore

AdapterView è un tipo speciale di widget che carica i dati in modo dinamico da un adattatore. L'esempio più comune di AdapterView è ListView. Diversamente dai widget statici come LinearLayout, nella gerarchia di visualizzazioni corrente può essere caricato solo un sottoinsieme dei AdapterView elementi secondari. Una semplice ricerca onView() non troverà le viste attualmente non caricate.

Espresso gestisce questo problema fornendo un punto di ingresso onData() separato che consente di caricare innanzitutto l'elemento dell'adattatore in questione, mettendolo a fuoco prima di utilizzare l'adattatore o uno dei suoi dispositivi secondari.

Avviso: le implementazioni personalizzate di AdapterView possono avere problemi con il metodo onData() se violano i contratti di ereditarietà, in particolare l'API getItem(). In questi casi, la cosa migliore da fare è il refactoring del codice dell'applicazione. In caso contrario, puoi implementare un AdapterViewProtocol personalizzato corrispondente. Per ulteriori informazioni, dai un'occhiata alla classe AdapterViewProtocols predefinita fornita da Espresso.

Test semplice della visualizzazione adattatore

Questo semplice test dimostra come utilizzare onData(). SimpleActivity contiene Spinner con alcuni elementi che rappresentano determinati tipi di bevande a base di caffè. Quando è selezionato un elemento, esiste un elemento TextView che cambia in "One %s a day!", dove %s rappresenta l'elemento selezionato.

L'obiettivo di questo test è aprire il Spinner, selezionare un articolo specifico e verificare che il TextView contenga l'articolo. Poiché la classe Spinner si basa su AdapterView, è consigliabile utilizzare onData() anziché onView() per la corrispondenza dell'elemento.

Apri la selezione dell'elemento

Kotlin

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

Java

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

Selezionare un elemento

Per la selezione dell'elemento, l'elemento Spinner crea un elemento ListView con i relativi contenuti. Questa visualizzazione può essere molto lunga e l'elemento potrebbe non essere contribuito alla gerarchia delle visualizzazioni. Con onData() forziamo l'elemento desiderato nella gerarchia delle visualizzazioni. Gli elementi in Spinner sono stringhe, quindi vogliamo trovare una corrispondenza per un elemento uguale alla stringa "Americano":

Kotlin

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

Java

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

Verifica che il testo sia corretto

Kotlin

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

Java

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

Debug

Espresso fornisce informazioni di debug utili quando un test ha esito negativo:

Logging

Espresso registra tutte le azioni di visualizzazione in logcat. Ecco alcuni esempi:

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

Visualizza gerarchia

Espresso stampa la gerarchia delle visualizzazioni nel messaggio di eccezione quando onView() non riesce.

  • Se onView() non trova la visualizzazione di destinazione, viene lanciato un NoMatchingViewException. Puoi esaminare la gerarchia delle viste nella stringa dell'eccezione per analizzare il motivo per cui il matcher non corrisponde ad alcuna vista.
  • Se onView() trova più visualizzazioni che corrispondono a un determinato matcher, viene lanciato un AmbiguousViewMatcherException. La gerarchia delle visualizzazioni viene stampata e tutte le visualizzazioni corrispondenti sono contrassegnate dall'etichetta 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****

In caso di una gerarchia di visualizzazioni complicata o di comportamenti imprevisti dei widget, è sempre utile utilizzare il visualizzatore gerarchia in Android Studio per avere una spiegazione.

Avvisi relativi alla visualizzazione dell'adattatore

Espresso avvisa gli utenti della presenza di AdapterView widget. Quando un'operazione onView() genera un widget NoMatchingViewException e AdapterView sono presenti nella gerarchia delle visualizzazioni, la soluzione più comune è utilizzare onData(). Il messaggio di eccezione include un avviso con un elenco delle visualizzazioni dell'adattatore. Puoi utilizzare queste informazioni per richiamare onData() e caricare la visualizzazione di destinazione.

Risorse aggiuntive

Per ulteriori informazioni sull'utilizzo di Espresso nei test Android, consulta le risorse seguenti.

Samples