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:
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:
onView(allOf(withText("7"), hasSibling(withText("item: 0"))))
.perform(click())
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:
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")))
}
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 est identique pour la barre d'action contextuelle:
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")))
}
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 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:
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")))
}
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:
Pour la barre d'action contextuelle, c'est encore très facile:
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")))
}
}
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 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:
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())))
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()
:
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())
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:
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
}
}
}
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
:
fun testDataItemNotInAdapter() {
onView(withId(R.id.list))
.check(matches(not(withAdaptedData(withItemContent("item: 168")))))
}
}
@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:
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)
}
}
}
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:
@Throws(Exception::class)
override fun setUp() {
super.setUp()
getActivity()
setFailureHandler(CustomFailureHandler(
ApplicationProvider.getApplicationContext<Context>()))
}
@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
:
onView(withText("South China Sea"))
.inRoot(withDecorView(not(`is`(getActivity().getWindow().getDecorView()))))
.perform(click())
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.
Faire correspondre un en-tête ou un pied de page dans une vue sous forme de liste
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 :
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)
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:
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))
}
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:
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())
// ...
}
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.