Ricette di caffè espresso

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:

Un'attività elenco che mostra 3 copie dello stesso elemento di visualizzazione all'interno di un elenco di 3 elementi

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 pulsante Salva si trova nella barra delle azioni, nella parte superiore dell'attività

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")));
}

Il pulsante di blocco si trova nella barra delle azioni, nella parte superiore dell'attività

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")));
}

Il pulsante del menu extra è visibile e viene visualizzato un elenco sotto la barra delle azioni nella parte superiore dello schermo.

Ecco come appare sui dispositivi con un pulsante del menu extra hardware:

Non è presente un pulsante del menu extra e viene visualizzato un elenco nella parte inferiore dello schermo

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")));
    }
}

Il pulsante del menu extra viene visualizzato nella barra delle azioni, mentre l&#39;elenco di opzioni viene visualizzato sotto la barra delle azioni, nella parte superiore dello schermo.

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.

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.