Questo documento descrive come configurare una serie di test Espresso comuni.
Abbinare una visualizzazione accanto a un'altra visualizzazione
Un layout potrebbe contenere alcune visualizzazioni non uniche di per sé. Ad esempio, un pulsante di chiamata ricorrente in una tabella di contatti potrebbe avere lo stesso R.id
, contenere lo stesso testo e le stesse proprietà degli altri pulsanti di chiamata nella gerarchia delle visualizzazioni.
Ad esempio, in questa attività, la visualizzazione con il testo "7"
si ripete su più righe:
Spesso, la visualizzazione non univoca è associata a un'etichetta univoca che si trova accanto, ad esempio un nome del contatto accanto al pulsante di chiamata. In questo caso, puoi utilizzare il matcher hasSibling()
per restringere la selezione:
Kotlin
onView(allOf(withText("7"), hasSibling(withText("item: 0")))) .perform(click())
Java
onView(allOf(withText("7"), hasSibling(withText("item: 0")))) .perform(click());
Abbina una visualizzazione all'interno di una barra delle azioni
ActionBarTestActivity
ha due diverse barre delle azioni: una normale e una contestuale creata da un menu opzioni. Entrambe
le barre delle azioni contengono un elemento sempre visibile e due elementi visibili solo
nel menu extra. Quando si fa clic su un elemento, viene modificato un TextView
con il contenuto dell'elemento su cui è stato fatto clic.
La corrispondenza delle icone visibili su entrambe le barre delle azioni è semplice, come mostrato nel seguente snippet di codice:
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"))); }
Il codice è identico per la barra delle azioni contestuale:
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"))); }
Fare clic sulle voci nel menu extra è un po' più complicato per la normale barra delle azioni perché alcuni dispositivi hanno un pulsante del menu extra dell'hardware che apre le voci extra in un menu Opzioni, mentre alcuni dispositivi hanno un pulsante del menu extra del software che apre un normale menu extra. Per fortuna, Espresso se ne occupa per noi.
Per la barra delle azioni normale:
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"))); }
Ecco come appare sui dispositivi con un pulsante del menu extra hardware:
Per la barra delle azioni contestuale è tutto molto semplice:
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"))); } }
Per visualizzare il codice completo di questi esempi, visualizza l'esempio di ActionBarTest.java
su GitHub.
Dichiarare che non viene mostrata una vista
Dopo aver eseguito una serie di azioni, ti consigliamo di asserire lo stato dell'interfaccia utente in fase di test. A volte può trattarsi di un caso negativo, come
ad esempio quando qualcosa non sta accadendo. Tieni presente che puoi trasformare qualsiasi matcher visualizzazione media in un ViewAssertion
utilizzando ViewAssertions.matches()
.
Nell'esempio seguente, prendiamo il matcher isDisplayed()
e lo invertiamo utilizzando il matcher not()
standard:
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())));
L'approccio descritto sopra funziona se la vista fa ancora parte della gerarchia. In caso contrario, riceverai un NoMatchingViewException
e dovrai utilizzare ViewAssertions.doesNotExist()
.
Dichiarare che non è presente una vista
Se la visualizzazione non rientra più nella gerarchia delle visualizzazioni, cosa che può succedere quando un'azione ha causato una transizione a un'altra attività, devi utilizzare 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());
Dichiara che un elemento di dati non è incluso in un adattatore
Per dimostrare che un determinato elemento di dati non si trova all'interno di un AdapterView
, devi fare le cose in modo leggermente diverso. Dobbiamo trovare il AdapterView
che ci interessa
e interrogare i dati che contiene. Non abbiamo bisogno di utilizzare onData()
.
Utilizziamo invece onView()
per trovare AdapterView
e poi utilizziamo un altro
matcher per lavorare sui dati all'interno della vista.
Per prima cosa il matcher:
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; } }; }
Allora basta onView()
per trovare 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"))))); } }
Abbiamo un'asserzione che non andrà a buon fine se esiste un elemento uguale a "item: 168" in una vista adattatore con l'elenco ID.
Per l'esempio completo, guarda il metodo testDataItemNotInAdapter()
nella classe AdapterViewTest.java
su GitHub.
Usa un gestore degli errori personalizzato
La sostituzione del valore FailureHandler
predefinito in Espresso con uno personalizzato consente una gestione degli errori aggiuntiva o diversa, come l'acquisizione di uno screenshot o la trasmissione di ulteriori informazioni di debug.
L'esempio CustomFailureHandlerTest
mostra come implementare un gestore di errori personalizzato:
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); } } }
Questo gestore degli errori genera MySpecialException
invece di NoMatchingViewException
e delega tutti gli altri errori a DefaultFailureHandler
. CustomFailureHandler
può essere registrato con espresso nel metodo setUp()
del test:
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())); }
Per ulteriori informazioni, consulta l'interfaccia FailureHandler
e
Espresso.setFailureHandler()
.
Scegliere come target finestre non predefinite
Android supporta più finestre. In genere, questo è trasparente per gli utenti e lo sviluppatore di app, ma in alcuni casi sono visibili più finestre, ad esempio quando una finestra di completamento automatico viene disegnata sopra la finestra principale dell'applicazione nel widget di ricerca. Per semplificare le cose, per impostazione predefinita Espresso utilizza un'euristica per
indovinare con quale Window
intendi interagire. Questa euristica è quasi sempre sufficiente; tuttavia, in rari casi, dovrai specificare la finestra a cui indirizzare l'interazione. Per farlo, fornisci il tuo matcher finestra principale o Root
matcher:
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());
Come nel caso di ViewMatchers
, forniamo un insieme di elementi RootMatchers
preconfezionati.
Ovviamente puoi sempre implementare il tuo oggetto Matcher
.
Dai un'occhiata all'esempio di multipleWindowTest su GitHub.
Associare un'intestazione o un piè di pagina in una visualizzazione elenco
Le intestazioni e i piè di pagina vengono aggiunti a ListViews
utilizzando i metodi addHeaderView()
e addFooterView()
. Per assicurarti che Espresso.onData()
sappia quale oggetto dati trovare
corrispondenza, passa un valore predefinito dell'oggetto dati come secondo parametro
a addHeaderView()
e addFooterView()
. Ecco alcuni esempi:
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);
Quindi, puoi scrivere un matcher per il piè di pagina:
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)); }
Caricare la visualizzazione in un test è banale:
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()); // ... }
Dai un'occhiata all'esempio di codice completo, disponibile nel metodo testClickFooter()
di
AdapterViewTest.java
su GitHub.