Principes de base d'Espresso

Ce document explique comment effectuer des tâches de test automatisées courantes à l'aide de l'API Espresso.

L'API Espresso encourage les auteurs de tests à réfléchir à ce qu'un utilisateur pourrait faire lorsqu'il interagit avec l'application : localiser les éléments d'interface utilisateur et interagir avec eux. Dans le même temps, le framework empêche l'accès direct aux activités et aux vues de l'application, car le fait de conserver ces objets et de les utiliser en dehors du thread UI constitue une source majeure de faiblesse de test. Ainsi, vous ne verrez pas de méthodes telles que getView() et getCurrentActivity() dans l'API Espresso. Vous pouvez toujours opérer en toute sécurité sur des vues en implémentant vos propres sous-classes de ViewAction et ViewAssertion.

Composants de l'API

Les principaux composants d'Espresso sont les suivants:

  • Espresso : point d'entrée des interactions avec les vues (via onView() et onData()). Expose également des API qui ne sont pas nécessairement liées à une vue, comme pressBack().
  • ViewMatchers : collection d'objets qui implémentent l'interface Matcher<? super View>. Vous pouvez transmettre un ou plusieurs de ces éléments à la méthode onView() pour localiser une vue dans la hiérarchie des vues actuelle.
  • ViewActions : collection d'objets ViewAction pouvant être transmis à la méthode ViewInteraction.perform(), par exemple click().
  • ViewAssertions : collection d'objets ViewAssertion pouvant être transmis à l'aide de la méthode ViewInteraction.check(). La plupart du temps, vous utiliserez l'assertion "matches", qui utilise un outil de mise en correspondance des vues pour valider l'état de la vue actuellement sélectionnée.

Exemple :

Kotlin

// withId(R.id.my_view) is a ViewMatcher
// click() is a ViewAction
// matches(isDisplayed()) is a ViewAssertion
onView(withId(R.id.my_view))
    .perform(click())
    .check(matches(isDisplayed()))

Java

// withId(R.id.my_view) is a ViewMatcher
// click() is a ViewAction
// matches(isDisplayed()) is a ViewAssertion
onView(withId(R.id.my_view))
    .perform(click())
    .check(matches(isDisplayed()));

Rechercher une vue

Dans la grande majorité des cas, la méthode onView() utilise un outil de mise en correspondance hamcrest censé correspondre à une (et à une seule) vue dans la hiérarchie des vues actuelle. Les outils de mise en correspondance sont puissants et connus de ceux qui les ont utilisés avec Mockito ou JUnit. Si vous n'êtes pas familier avec les outils de mise en correspondance hamcrest, nous vous suggérons de commencer par un rapide coup d'œil à cette présentation.

Souvent, la vue souhaitée possède un R.id unique et un simple outil de mise en correspondance withId permet de réduire la recherche de vues. Toutefois, il existe de nombreux cas légitimes où vous ne pouvez pas déterminer R.id au moment du développement du test. Par exemple, la vue spécifique peut ne pas avoir de R.id ou le R.id n'est pas unique. Cela peut rendre les tests d'instrumentation normaux fragiles et compliqués à écrire, car la méthode normale d'accès à la vue (avec findViewById()) ne fonctionne pas. Ainsi, vous devrez peut-être accéder à des membres privés de l'activité ou du fragment qui contiennent la vue, ou rechercher un conteneur avec un R.id connu et accéder à son contenu pour la vue particulière.

Espresso gère ce problème correctement en vous permettant d'affiner la vue à l'aide d'objets ViewMatcher existants ou de vos propres objets personnalisés.

Pour rechercher une vue à l'aide de son R.id, il suffit d'appeler onView():

Kotlin

onView(withId(R.id.my_view))

Java

onView(withId(R.id.my_view));

Parfois, les valeurs R.id sont partagées entre plusieurs vues. Lorsque cela se produit, une tentative d'utilisation d'un R.id particulier génère une exception, telle que AmbiguousViewMatcherException. Le message d'exception fournit une représentation textuelle de la hiérarchie des vues actuelle, dans laquelle vous pouvez rechercher et trouver les vues correspondant à l'élément R.id non unique:

java.lang.RuntimeException:
androidx.test.espresso.AmbiguousViewMatcherException
This matcher matches multiple views in the hierarchy: (withId: is <123456789>)

...

+----->SomeView{id=123456789, res-name=plus_one_standard_ann_button,
visibility=VISIBLE, width=523, height=48, has-focus=false, has-focusable=true,
window-focus=true, is-focused=false, is-focusable=false, enabled=true,
selected=false, is-layout-requested=false, text=,
root-is-layout-requested=false, x=0.0, y=625.0, child-count=1}
****MATCHES****
|
+------>OtherView{id=123456789, res-name=plus_one_standard_ann_button,
visibility=VISIBLE, width=523, height=48, has-focus=false, has-focusable=true,
window-focus=true, is-focused=false, is-focusable=true, enabled=true,
selected=false, is-layout-requested=false, text=Hello!,
root-is-layout-requested=false, x=0.0, y=0.0, child-count=1}
****MATCHES****

En examinant les différents attributs des vues, vous pouvez trouver des propriétés identifiables de manière unique. Dans l'exemple ci-dessus, l'une des vues contient le texte "Hello!". Vous pouvez l'utiliser pour affiner votre recherche à l'aide de combinaisons de correspondances:

Kotlin

onView(allOf(withId(R.id.my_view), withText("Hello!")))

Java

onView(allOf(withId(R.id.my_view), withText("Hello!")));

Vous pouvez également choisir de n'inverser aucun des outils de mise en correspondance:

Kotlin

onView(allOf(withId(R.id.my_view), not(withText("Unwanted"))))

Java

onView(allOf(withId(R.id.my_view), not(withText("Unwanted"))));

Consultez ViewMatchers pour obtenir les outils de mise en correspondance des vues fournis par Espresso.

Points à prendre en compte

  • Dans une application bien conçue, toutes les vues avec lesquelles un utilisateur peut interagir doivent contenir un texte descriptif ou une description du contenu. Pour en savoir plus, consultez la page Rendre les applications plus accessibles. Si vous ne parvenez pas à affiner une recherche à l'aide de withText() ou de withContentDescription(), envisagez de la considérer comme un bug d'accessibilité.
  • Utilisez l'outil de mise en correspondance le moins descriptif qui trouve la vue que vous recherchez. Ne spécifiez pas trop d'informations, car cela obligerait le framework à effectuer plus de travail que nécessaire. Par exemple, si une vue est identifiable de manière unique par son texte, vous n'avez pas besoin de spécifier qu'elle peut également être attribuée à partir de TextView. Dans de nombreuses vues, l'élément R.id de la vue doit être suffisant.
  • Si la vue cible se trouve dans un élément AdapterView tel que ListView, GridView ou Spinner, la méthode onView() risque de ne pas fonctionner. Dans ce cas, utilisez plutôt onData().

Effectuer une action sur une vue

Lorsque vous avez trouvé un outil de mise en correspondance adapté à la vue cible, il est possible d'y exécuter des instances de ViewAction à l'aide de la méthode perform.

Par exemple, pour cliquer sur la vue:

Kotlin

onView(...).perform(click())

Java

onView(...).perform(click());

Vous pouvez exécuter plusieurs actions avec un seul appel "Perform" :

Kotlin

onView(...).perform(typeText("Hello"), click())

Java

onView(...).perform(typeText("Hello"), click());

Si la vue que vous utilisez se trouve dans un ScrollView (vertical ou horizontal), envisagez les actions précédentes qui nécessitent l'affichage de la vue, telles que click() et typeText(), avec scrollTo(). Cela permet de s'assurer que la vue est affichée avant de passer à l'autre action:

Kotlin

onView(...).perform(scrollTo(), click())

Java

onView(...).perform(scrollTo(), click());

Consultez ViewActions pour connaître les actions d'affichage fournies par Espresso.

Vérifier les assertions de vue

Les assertions peuvent être appliquées à la vue actuellement sélectionnée à l'aide de la méthode check(). L'assertion la plus utilisée est matches(). Elle utilise un objet ViewMatcher pour revendiquer l'état de la vue actuellement sélectionnée.

Par exemple, pour vérifier qu'une vue comporte le texte "Hello!":

Kotlin

onView(...).check(matches(withText("Hello!")))

Java

onView(...).check(matches(withText("Hello!")));

Si vous souhaitez affirmer que "Hello!" est le contenu de la vue, ce qui suit est considéré comme une mauvaise pratique:

Kotlin

// Don't use assertions like withText inside onView.
onView(allOf(withId(...), withText("Hello!"))).check(matches(isDisplayed()))

Java

// Don't use assertions like withText inside onView.
onView(allOf(withId(...), withText("Hello!"))).check(matches(isDisplayed()));

En revanche, si vous souhaitez affirmer qu'une vue avec le texte "Hello!" est visible (par exemple, après une modification de l'indicateur de visibilité des vues), le code est parfait.

Afficher le test simple d'assertion

Dans cet exemple, SimpleActivity contient une Button et une TextView. Lorsque l'utilisateur clique sur le bouton, le contenu de TextView devient "Hello Espresso!".

Voici comment tester cela avec Espresso:

Cliquez sur le bouton

La première étape consiste à rechercher une propriété permettant de trouver le bouton. Comme prévu, le bouton dans SimpleActivity possède un R.id unique.

Kotlin

onView(withId(R.id.button_simple))

Java

onView(withId(R.id.button_simple));

Maintenant, pour effectuer le clic:

Kotlin

onView(withId(R.id.button_simple)).perform(click())

Java

onView(withId(R.id.button_simple)).perform(click());

Vérifier le texte TextView

Le TextView avec le texte à vérifier possède également un R.id unique:

Kotlin

onView(withId(R.id.text_simple))

Java

onView(withId(R.id.text_simple));

Maintenant, pour vérifier le texte du contenu:

Kotlin

onView(withId(R.id.text_simple)).check(matches(withText("Hello Espresso!")))

Java

onView(withId(R.id.text_simple)).check(matches(withText("Hello Espresso!")));

Vérifier le chargement des données dans les vues d'adaptateur

AdapterView est un type spécial de widget qui charge ses données de manière dynamique à partir d'un adaptateur. L'exemple le plus courant de AdapterView est ListView. Contrairement aux widgets statiques tels que LinearLayout, seul un sous-ensemble des enfants AdapterView peut être chargé dans la hiérarchie des vues actuelle. Une simple recherche onView() ne permet pas de trouver les vues qui ne sont pas actuellement chargées.

Espresso gère cela en fournissant un point d'entrée onData() distinct qui peut d'abord charger l'élément d'adaptateur en question, le mettant en évidence avant d'effectuer une opération sur lui ou sur l'un de ses enfants.

Avertissement:Les implémentations personnalisées de AdapterView peuvent rencontrer des problèmes avec la méthode onData() si elles rompent les contrats d'héritage, en particulier l'API getItem(). Dans ce cas, le meilleur plan d'action consiste à refactoriser le code de votre application. Si vous ne pouvez pas le faire, vous pouvez implémenter un AdapterViewProtocol personnalisé correspondant. Pour en savoir plus, consultez la classe AdapterViewProtocols par défaut fournie par Espresso.

Test simple de vue de l'adaptateur

Ce test simple montre comment utiliser onData(). SimpleActivity contient un Spinner avec quelques éléments qui représentent des types de boissons à base de café. Lorsqu'un élément est sélectionné, TextView devient "One %s a day!", où %s représente l'élément sélectionné.

L'objectif de ce test est d'ouvrir Spinner, de sélectionner un élément spécifique et de vérifier que TextView contient l'élément. La classe Spinner étant basée sur AdapterView, il est recommandé d'utiliser onData() au lieu de onView() pour faire correspondre l'élément.

Ouvrir la sélection d'éléments

Kotlin

onView(withId(R.id.spinner_simple)).perform(click())

Java

onView(withId(R.id.spinner_simple)).perform(click());

Sélectionner un élément

Pour la sélection de l'élément, Spinner crée une ListView avec son contenu. Cette vue peut être très longue, et l'élément peut ne pas être ajouté à la hiérarchie des vues. En utilisant onData(), nous forcerons l'élément souhaité dans la hiérarchie des vues. Les éléments de Spinner sont des chaînes. Nous voulons donc faire correspondre un élément égal à la chaîne "Americano":

Kotlin

onData(allOf(`is`(instanceOf(String::class.java)),
        `is`("Americano"))).perform(click())

Java

onData(allOf(is(instanceOf(String.class)), is("Americano"))).perform(click());

Vérifiez que le texte est correct

Kotlin

onView(withId(R.id.spinnertext_simple))
    .check(matches(withText(containsString("Americano"))))

Java

onView(withId(R.id.spinnertext_simple))
    .check(matches(withText(containsString("Americano"))));

Débogage

Espresso fournit des informations de débogage utiles en cas d'échec d'un test:

Journalisation

Espresso enregistre toutes les actions de vue dans logcat. Par exemple :

ViewInteraction: Performing 'single click' action on view with text: Espresso

Hiérarchie de vues

Espresso imprime la hiérarchie des vues dans le message d'exception en cas d'échec de onView().

  • Si onView() ne trouve pas la vue cible, une NoMatchingViewException est générée. Vous pouvez examiner la hiérarchie des vues dans la chaîne d'exception pour analyser la raison pour laquelle l'outil de mise en correspondance ne correspond à aucune vue.
  • Si onView() trouve plusieurs vues qui correspondent à l'outil de mise en correspondance donné, une exception AmbiguousViewMatcherException est générée. La hiérarchie des vues est imprimée et toutes les vues mises en correspondance sont marquées à l'aide du libellé MATCHES:
java.lang.RuntimeException:
androidx.test.espresso.AmbiguousViewMatcherException
This matcher matches multiple views in the hierarchy: (withId: is <123456789>)

...

+----->SomeView{id=123456789, res-name=plus_one_standard_ann_button,
visibility=VISIBLE, width=523, height=48, has-focus=false, has-focusable=true,
window-focus=true, is-focused=false, is-focusable=false, enabled=true,
selected=false, is-layout-requested=false, text=,
root-is-layout-requested=false, x=0.0, y=625.0, child-count=1}
****MATCHES****
|
+------>OtherView{id=123456789, res-name=plus_one_standard_ann_button,
visibility=VISIBLE, width=523, height=48, has-focus=false, has-focusable=true,
window-focus=true, is-focused=false, is-focusable=true, enabled=true,
selected=false, is-layout-requested=false, text=Hello!,
root-is-layout-requested=false, x=0.0, y=0.0, child-count=1}
****MATCHES****

En cas de hiérarchie des vues complexe ou de comportement inattendu de widgets, il est toujours utile d'utiliser Hierarchy Viewer dans Android Studio pour obtenir des explications.

Avertissements concernant les vues de l'adaptateur

Espresso avertit les utilisateurs de la présence de widgets AdapterView. Lorsqu'une opération onView() génère une NoMatchingViewException et que des widgets AdapterView sont présents dans la hiérarchie des vues, la solution la plus courante consiste à utiliser onData(). Le message d'exception inclura un avertissement avec la liste des vues de l'adaptateur. Vous pouvez utiliser ces informations pour appeler onData() afin de charger la vue cible.

Ressources supplémentaires

Pour en savoir plus sur l'utilisation d'Espresso dans les tests Android, consultez les ressources suivantes.

Exemples