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:
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 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"))); }
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"))); }
Voici à quoi cela ressemble sur les appareils dotés d'un bouton de menu à développer matériel:
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"))); } }
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.
Faire correspondre un en-tête ou un pied de page dans une liste
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.