Ricette di caffè espresso

Questo documento descrive come configurare una serie di test per caffè espresso comuni.

Associare una vista accanto a un'altra vista

Un layout può contenere determinate visualizzazioni non univoche. Per ad esempio, un pulsante di chiamata ripetuto in una tabella di contatti potrebbe avere lo stesso R.id, contengono lo stesso testo e hanno le stesse proprietà di un'altra chiamata nella gerarchia di visualizzazione.

Ad esempio, in questa attività, la visualizzazione con il testo "7" si ripete in più righe:

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

Spesso, la vista non univoca viene associata a un'etichetta univoca che si trova ad esempio il nome del contatto accanto al pulsante di chiamata. In questo caso, puoi utilizzare la corrispondenza 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());

Associare una visualizzazione all'interno di una barra delle azioni

ActionBarTestActivity ha due diverse barre di azione: una barra delle azioni e una barra delle azioni contestuali creata da un menu delle opzioni. Entrambi le barre delle azioni hanno un elemento sempre visibile e due elementi visibile nel menu extra. Quando viene fatto clic su un elemento, un TextView viene modificato in contenuti dell'articolo 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 sulla barra delle azioni, nella parte superiore dell'attività.

Il codice è identico per la barra delle azioni contestuali:

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 sulla barra delle azioni, nella parte superiore dell'attività.

Fare clic sugli elementi nel menu extra è un po' più complicato per la normale azione barra perché alcuni dispositivi hanno un pulsante del menu extra hardware, che apre la degli elementi in eccesso in un menu delle opzioni e alcuni dispositivi hanno un overflow del software che apre un normale menu extra. Fortunatamente, 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 il
          barra delle azioni nella parte superiore dello schermo

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

Non è presente alcun pulsante del menu extra e nella parte inferiore viene visualizzato un elenco
          della schermata

Per quanto riguarda la barra delle azioni contestuali, l'operazione è ancora 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 e l&#39;elenco di
          le opzioni vengono visualizzate sotto la barra delle azioni, nella parte superiore dello schermo

Per vedere il codice completo per questi esempi, visualizza la Esempio di ActionBarTest.java su GitHub.

Dichiarare che una vista non è mostrata

Dopo aver eseguito una serie di azioni, sicuramente vorrai affermare lo stato dell'UI sottoposta a test. A volte potrebbe trattarsi di un caso negativo, ad esempio quando se qualcosa non sta succedendo. Ricorda che puoi attivare qualsiasi visualizzazione "Hamcrest" matcher 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 visualizzazione fa ancora parte della gerarchia. Se è non riceverai un NoMatchingViewException e dovrai usare ViewAssertions.doesNotExist().

Dichiarare che non è presente una vista

Se la visualizzazione non rientra nella gerarchia delle visualizzazioni, questo può accadere quando ha causato una transizione a un'altra attività; devi usare 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());

Dichiarare che un elemento dati non si trova in un adattatore

Per dimostrare che un determinato dato non si trova all'interno di un AdapterView è necessario le cose in modo un po' diverso. Dobbiamo trovare il AdapterView che ci interessa e interrogare i dati in suo possesso. Non è necessario utilizzare onData(). Utilizziamo invece onView() per trovare AdapterView e poi un altro per lavorare sui dati all'interno della vista.

Per prima cosa, l'abbinamento:

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

Poi 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'affermazione che non riuscirà se un elemento uguale a "item: 168" esiste in una vista adattatore con l'elenco di ID.

Per l'esempio completo, guarda il metodo testDataItemNotInAdapter() all'interno della AdapterViewTest.java su GitHub.

Utilizza un gestore degli errori personalizzato

La sostituzione del valore FailureHandler predefinito di Espresso con uno personalizzato consente gestione degli errori aggiuntiva o diversa, come l'acquisizione di uno screenshot o il passaggio insieme a informazioni di debug aggiuntive.

L'esempio CustomFailureHandlerTest mostra come implementare una richiesta gestore degli errori:

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 un MySpecialException anziché un NoMatchingViewException e delega tutti gli altri errori al DefaultFailureHandler. È possibile registrare CustomFailureHandler 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 FailureHandler a riga di comando e Espresso.setFailureHandler().

Scegli come target finestre non predefinite

Android supporta più finestre. Solitamente, è trasparente per gli utenti e lo sviluppatore dell'app, ma in alcuni casi sono visibili più finestre, ad esempio come quando una finestra di completamento automatico viene disegnata sopra la finestra principale dell'applicazione il widget di ricerca. Per semplificare le cose, per impostazione predefinita Espresso usa un approccio euristico indovina con quale Window intendi interagire. Questa euristica è quasi è sempre abbastanza buono; Tuttavia, in rari casi, dovrai specificare la finestra deve essere scelto come target da un'interazione. Puoi farlo fornendo la tua finestra root o Root:

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, sono forniti un set di dati RootMatchers. Naturalmente, puoi sempre implementare il tuo oggetto Matcher.

Dai un'occhiata a MultipleWindowTest esempio su GitHub.

Intestazioni e piè di pagina vengono aggiunti a ListViews utilizzando addHeaderView() e addFooterView(). Per assicurarti che Espresso.onData() sappia quale oggetto dati assicurati di trasmettere un valore di oggetto dati preimpostato come secondo parametro a addHeaderView() e addFooterView(). Ad esempio:

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 è un'operazione semplice:

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.