Recettes d'Espresso

Ce document explique comment configurer différents tests Espresso courants.

Faire correspondre une vue à une autre

Une mise en page peut contenir des vues qui ne sont pas uniques en elles-mêmes. Par exemple, un bouton d'appel répété dans une table de contacts peut avoir le même R.id, contenir le même texte et avoir les mêmes propriétés que d'autres boutons d'appel dans la hiérarchie des vues.

Par exemple, dans cette activité, la vue avec le texte "7" se répète sur plusieurs lignes:

Activité de liste affichant trois copies du même élément de vue dans une liste à trois éléments

Souvent, la vue non unique est associée à un libellé unique situé à côté, comme le nom du contact à côté du bouton d'appel. Dans ce cas, vous pouvez utiliser l'outil de mise en correspondance hasSibling() pour affiner votre sélection:

Kotlin

onView(allOf(withText("7"), hasSibling(withText("item: 0"))))
    .perform(click())

Java

onView(allOf(withText("7"), hasSibling(withText("item: 0"))))
    .perform(click());

Faire correspondre une vue située à l'intérieur d'une barre d'action

ActionBarTestActivity comporte deux barres d'action différentes: une barre d'action normale et une barre d'action contextuelle créée à partir d'un menu d'options. Les deux barres d'action comportent un élément toujours visible et deux éléments visibles uniquement dans le menu à développer. Lorsqu'un utilisateur clique sur un élément, un TextView est remplacé par le contenu de cet élément.

La mise en correspondance des icônes visibles sur les deux barres d'action est simple, comme illustré dans l'extrait de code suivant:

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

Le bouton d'enregistrement se trouve sur la barre d'action, en haut de l'activité.

Le code de la barre d'action contextuelle semble identique:

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

Le bouton de verrouillage se trouve sur la barre d'action, en haut de l'activité.

Cliquer sur les éléments du menu à développer est un peu plus délicat pour la barre d'action normale, car certains appareils disposent d'un bouton matériel de menu à développer qui ouvre les éléments en trop dans un menu d'options, et certains appareils disposent d'un bouton de menu à développer logiciel qui ouvre un menu à développer normal. Heureusement, Espresso s'en charge pour nous.

Pour la barre d'action 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")));
}

Le bouton du menu à développer est visible, et une liste s&#39;affiche sous la barre d&#39;action en haut de l&#39;écran.

Voici à quoi cela ressemble sur les appareils dotés d'un bouton de menu à développer matériel:

Il n&#39;y a pas de bouton de menu à développer, et une liste s&#39;affiche en bas de l&#39;écran

Là encore, pour la barre d'action contextuelle, c'est très facile:

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

Le bouton du menu à développer apparaît dans la barre d&#39;action, et la liste des options s&#39;affiche sous la barre d&#39;action, en haut de l&#39;écran.

Pour afficher le code complet de ces exemples, consultez l'exemple ActionBarTest.java sur GitHub.

Déclarer qu'une vue n'est pas affichée

Après avoir effectué une série d'actions, vous souhaiterez certainement vérifier l'état de l'interface utilisateur testée. Parfois, il peut s'agir d'un cas négatif (par exemple, en l'absence de problème). N'oubliez pas que vous pouvez transformer n'importe quel outil de mise en correspondance des vues hamcrest en ViewAssertion à l'aide de ViewAssertions.matches().

Dans l'exemple ci-dessous, nous utilisons l'outil de mise en correspondance isDisplayed() et l'inverse à l'aide de l'outil de mise en correspondance 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'approche ci-dessus fonctionne si la vue fait toujours partie de la hiérarchie. Si ce n'est pas le cas, vous obtiendrez une NoMatchingViewException et vous devrez utiliser ViewAssertions.doesNotExist().

Déclarer qu'une vue n'est pas présente

Si la vue disparaît de la hiérarchie des vues (ce qui peut se produire lorsqu'une action a entraîné une transition vers une autre activité), vous devez utiliser 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());

Déclarer qu'un élément de données ne se trouve pas dans un adaptateur

Pour prouver qu'un élément de données particulier ne se trouve pas dans une AdapterView, vous devez procéder un peu différemment. Nous devons trouver les AdapterView qui nous intéressent et interroger les données qu'elles contiennent. Nous n'avons pas besoin d'utiliser onData(). À la place, nous utilisons onView() pour trouver AdapterView, puis nous utilisons un autre outil de mise en correspondance pour travailler sur les données dans la vue.

D'abord l'outil de mise en correspondance:

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

Ensuite, tout ce dont nous avons besoin est onView() pour trouver 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")))));
    }
}

Nous avons également une assertion qui échouera si un élément égal à "item: 168" existe dans une vue d'adaptateur avec la liste d'ID.

Pour obtenir l'exemple complet, consultez la méthode testDataItemNotInAdapter() dans la classe AdapterViewTest.java sur GitHub.

Utiliser un gestionnaire d'échecs personnalisé

Le remplacement de la valeur par défaut FailureHandler dans Espresso par une valeur personnalisée permet de gérer des erreurs supplémentaires ou différentes, par exemple pour effectuer une capture d'écran ou transmettre des informations de débogage supplémentaires.

L'exemple CustomFailureHandlerTest montre comment implémenter un gestionnaire de défaillances personnalisé:

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

Ce gestionnaire d'échecs génère une erreur MySpecialException au lieu d'une NoMatchingViewException et délègue tous les autres échecs à DefaultFailureHandler. Le CustomFailureHandler peut être enregistré avec Espresso dans la méthode setUp() du 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()));
}

Pour en savoir plus, consultez l'interface FailureHandler et Espresso.setFailureHandler().

Cibler des fenêtres autres que celles par défaut

Android est compatible avec plusieurs fenêtres. Normalement, cela est transparent pour les utilisateurs et le développeur de l'application. Toutefois, dans certains cas, plusieurs fenêtres sont visibles, par exemple lorsqu'une fenêtre de saisie semi-automatique est dessinée sur la fenêtre principale de l'application dans le widget de recherche. Pour simplifier les choses, Espresso utilise par défaut une heuristique pour déterminer avec quel Window vous souhaitez interagir. Cette heuristique est presque toujours suffisante. Toutefois, dans de rares cas, vous devrez spécifier la fenêtre qu'une interaction doit cibler. Pour ce faire, fournissez votre propre outil de mise en correspondance de fenêtres racine, ou 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());

Comme pour ViewMatchers, nous fournissons un ensemble d'éléments RootMatchers prédéfinis. Bien entendu, vous pouvez toujours implémenter votre propre objet Matcher.

Consultez l'exemple MultipleWindowTest sur GitHub.

Les en-têtes et les pieds de page sont ajoutés à ListViews à l'aide des méthodes addHeaderView() et addFooterView(). Pour vous assurer que Espresso.onData() sait quel objet de données mettre en correspondance, veillez à transmettre une valeur d'objet de données prédéfinie en tant que deuxième paramètre à addHeaderView() et addFooterView(). Par exemple :

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

Ensuite, vous pouvez écrire une mise en correspondance pour le pied de page:

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

Et le chargement de la vue dans un test est simple:

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

    // ...
}

Consultez l'exemple de code complet disponible dans la méthode testClickFooter() de AdapterViewTest.java sur GitHub.