Recettes d'Espresso

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

Faire correspondre une vue à côté d'une autre

Une mise en page peut contenir certaines vues qui ne sont pas uniques en soi. Pour exemple, un bouton d'appel répété dans une table de contacts peut avoir le même R.id, contiennent le même texte et possèdent les mêmes propriétés que les autres appels 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 montrant trois copies du même élément "view"
     dans une liste à trois éléments

Souvent, la vue non unique sera associée à une étiquette unique qui se trouve qui se trouve à côté, comme son nom à 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 à l'intérieur d'une barre d'action

Le ActionBarTestActivity comporte deux barres d'action différentes: une barre normale une barre d'action et une barre d'action contextuelle créée à partir d'un menu d'options. Les deux Les barres d'action comportent un élément toujours visible et deux éléments qui ne sont visible dans le menu à développer. Lorsqu'un utilisateur clique sur un élément, un TextView devient le contenu de l'article sur lequel l'utilisateur a cliqué.

Il est facile de faire correspondre les icônes visibles des deux barres d'action, 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 "Enregistrer" se trouve dans la barre d'action, en haut de l'activité

Le code est identique pour la barre d'action contextuelle:

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 dans la barre d'action, en haut de l'activité

Cliquer sur des éléments du menu à développer est un peu plus délicat pour l'action normale car certains appareils ont un bouton de menu à développer matérielle, qui ouvre le certains éléments d'un menu d'options, et certains appareils ont un menu à développer qui ouvre un menu à développer normal. Heureusement, Espresso gère cela 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 apparaît sous le bouton
          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:

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

Pour la barre d'action contextuelle, c'est encore 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;affichent sous la barre d&#39;action, en haut de l&#39;écran

Pour afficher le code complet de ces exemples, consultez la Exemple ActionBarTest.java sur GitHub

Déclarer qu'une vue ne s'affiche pas

Après avoir effectué une série d'actions, vous devrez certainement l'état de l'UI testée. Il peut s'agir d'un cas négatif, par exemple quelque chose ne se passe pas. Gardez à l'esprit que vous pouvez transformer n'importe quelle vue Hamcrest de mise en correspondance dans un ViewAssertion à l'aide de ViewAssertions.matches().

Dans l'exemple ci-dessous, nous prenons l'outil de mise en correspondance isDisplayed() et l'inversons en utilisant 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 c'est le cas non, vous obtiendrez un NoMatchingViewException et vous devrez utiliser ViewAssertions.doesNotExist()

Déclarer qu'aucune vue n'est présente

Si la vue a disparu de la hiérarchie des vues, ce qui peut se produire lorsqu'une a entraîné une transition vers une autre activité. 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 les choses un peu différemment. Nous devons trouver les AdapterView qui nous intéressent et d'interroger les données qu'elles détiennent. Nous n'avons pas besoin d'utiliser onData(). À la place, nous utilisons onView() pour trouver AdapterView, puis utilisons un autre pour qu'il fonctionne sur les données dans la vue.

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

Et nous avons une assertion qui échoue si un élément est égal à "item: 168". existe dans une vue d'adaptateur avec la liste d'ID.

Pour voir l'exemple complet, examinez la méthode testDataItemNotInAdapter() dans la AdapterViewTest.java sur GitHub.

Utiliser un gestionnaire d'échecs personnalisé

Le remplacement de la valeur FailureHandler par défaut dans Espresso par une valeur personnalisée permet d'obtenir gestion d'erreurs supplémentaires ou différentes, comme une capture d'écran ou la transmission ainsi que des informations de débogage supplémentaires.

L'exemple CustomFailureHandlerTest montre comment implémenter un gestionnaire d'échecs:

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 exception MySpecialException au lieu d'une NoMatchingViewException et délègue toutes les autres défaillances au DefaultFailureHandler Le CustomFailureHandler peut être enregistré auprès de 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 les FailureHandler de commande et Espresso.setFailureHandler()

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

Android prend en charge plusieurs fenêtres. Normalement, cette information est transparente pour les utilisateurs. et le développeur de l'application, mais dans certains cas, plusieurs fenêtres sont visibles, comme lorsqu'une fenêtre de saisie semi-automatique s'affiche sur la fenêtre principale de l'application dans le widget Recherche. Pour simplifier les choses, Espresso utilise par défaut une heuristique pour devinez avec quel Window vous avez l'intention d'interagir. Cette heuristique est presque toujours assez bon ; Toutefois, dans de rares cas, vous devrez préciser une interaction doit cibler. Vous pouvez le faire en fournissant votre propre fenêtre 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 c'est le cas pour ViewMatchers nous fournissons un ensemble RootMatchers Bien entendu, vous pouvez toujours implémenter votre propre objet Matcher.

Examinons la fonction MultipleWindowTest. exemple sur GitHub.

Les en-têtes et les pieds de page sont ajoutés à ListViews à l'aide des balises addHeaderView() et addFooterView(). Pour s'assurer que Espresso.onData() sait quel objet de données pour la mise en correspondance, assurez-vous de transmettre une valeur d'objet de données prédéfinie en tant que deuxième paramètre à addHeaderView() et addFooterView(). 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 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));
}

De plus, le chargement de la vue lors d'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());

    // ...
}

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