Przepisy na espresso

W tym dokumencie opisujemy, jak skonfigurować różne popularne testy Espresso.

Dopasowanie widoku do innego widoku

Układ może zawierać określone widoki, które same z siebie nie są unikalne. Dla: Na przykład przycisk powtarzania połączenia w tabeli kontaktów może mieć taki sam R.id, zawierają ten sam tekst i mają te same właściwości co inne wywołanie w hierarchii widoków.

Na przykład w tej aktywności widok z tekstem "7" powtarza się w wielu miejscach wiersze:

Aktywność związana z listą pokazującą 3 kopie tego samego elementu widoku
     na liście złożonej z 3 produktów.

Nieunikalny widok jest często powiązany z unikalną etykietą umieszczoną obok niej, np. nazwę kontaktu obok przycisku połączenia. W tym przypadku za pomocą dopasowania hasSibling() możesz zawęzić wybór:

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

Dopasowanie do widoku w pasku działań

ActionBarTestActivity ma 2 różne paski działań: normalny paska działań i paska działań kontekstowych utworzonego z menu opcji. Obie opcje Pasek działań zawiera 1 zawsze widoczny element i 2 elementy widoczne w rozszerzonym menu. Kliknięcie elementu powoduje zmianę obiektu TextView na treść klikniętego elementu.

Jak widać, dopasowanie widocznych ikon na obu paskach działań jest proste. w tym fragmencie kodu:

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

Przycisk zapisywania znajduje się na pasku działań u góry aktywności

Kod wygląda identycznie w przypadku paska działań kontekstowych:

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

Przycisk blokady znajduje się na pasku działań u góry aktywności

Klikanie elementów w rozszerzonym menu jest nieco trudniejsze do wykonania w zwykłym trybie. bo niektóre urządzenia mają sprzętowy przycisk rozszerzonego menu, który otwiera dodatkowe pozycje w menu opcji, a na niektórych urządzeniach występuje nadmiar zawartości oprogramowania przycisk menu, który otwiera normalne rozszerzone menu. Na szczęście Espresso radzi sobie dla nas.

W przypadku zwykłego paska działań:

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

Przycisk rozszerzonego menu jest widoczny, a poniżej
          pasek działań u góry ekranu

Na urządzeniach z przyciskiem rozszerzonego menu sprzętowego ta strona wygląda tak:

Na dole nie ma przycisku rozszerzonego menu, a na dole widać listę
          ekranu

W przypadku paska działań kontekstowych jest to bardzo proste:

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

Na pasku działań pojawi się przycisk rozszerzonego menu, a lista
          opcje znajdują się pod paskiem działań, u góry ekranu

Aby zobaczyć pełny kod tych przykładów, wyświetl ActionBarTest.java – przykład w GitHubie.

Potwierdzanie, że widok nie jest wyświetlany

Po wykonaniu serii działań warto potwierdzić, że w testowanym stanie interfejsu. Czasami może to być negatywne, np. że coś się nie dzieje. Pamiętaj, że w dowolnym widoku możesz włączyć widok hamcrest, dopasowania do funkcji ViewAssertion przy użyciu funkcji ViewAssertions.matches().

W przykładzie poniżej stosujemy dopasowanie isDisplayed() i odwracamy je za pomocą funkcji standardowe dopasowanie not():

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

Powyższe podejście sprawdza się, jeśli widok nadal jest częścią hierarchii. Jeśli tak nie, otrzymasz NoMatchingViewException i konieczne będzie użycie ViewAssertions.doesNotExist()

Potwierdzanie braku widoku

Jeśli widok danych został usunięty z hierarchii widoków – co może się zdarzyć, gdy spowodowało przejście do innej aktywności. ViewAssertions.doesNotExist():

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

Trzeba twierdzić, że element danych nie znajduje się w adapterze

Aby udowodnić, że dany element danych nie znajduje się w usłudze AdapterView, musisz wykonać te czynności wszystko wygląda nieco inaczej. Musimy znaleźć AdapterView, który nas interesuje i przeanalizuje przechowywane dane. Nie musimy używać funkcji onData(). Zamiast tego używamy wyrażenia onView(), aby znaleźć AdapterView, a następnie innej na podstawie danych w widoku.

Najpierw dopasowanie:

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

Potem wystarczy tylko onView(), aby znaleźć AdapterView:

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

Mamy też potwierdzenie, które nie powiedzie się, jeśli element równy „item: 168”. znajduje się w widoku adaptacyjnym z listą identyfikatorów.

Pełną próbkę znajdziesz w metodzie testDataItemNotInAdapter() w sekcji AdapterViewTest.java. znajdziesz na GitHubie.

Użyj niestandardowego modułu obsługi błędów

Zastępowanie domyślnej wartości FailureHandler w Espresso na niestandardową umożliwia: dodatkowe lub inne sposoby obsługi błędów, np. robienie zrzutu ekranu, wraz z dodatkowymi informacjami na temat debugowania.

Przykład żądania CustomFailureHandlerTest pokazuje, jak wdrożyć niestandardową regułę moduł obsługi błędów:

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

Ten moduł obsługi błędów zwraca MySpecialException zamiast NoMatchingViewException i przekazuje wszystkie pozostałe błędy do DefaultFailureHandler Urządzenie CustomFailureHandler można zarejestrować w usłudze Espresso w metodzie setUp() testu:

KotlinJava
@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()));
}

Więcej informacji: FailureHandler i Espresso.setFailureHandler()

Kieruj reklamy na okna inne niż domyślne

Android obsługuje wiele okien. Zwykle te informacje są przejrzyste dla użytkowników. i deweloperem aplikacji, ale w niektórych przypadkach widocznych jest wiele okien, np. okno autouzupełniania wyświetlane na głównym oknie aplikacji w widżecie wyszukiwania. Aby uprościć ten proces, Espresso domyślnie korzysta z algorytmu heurystycznego, odgadnij, z którą usługą Window chcesz wejść w interakcję. Ta heurystyka jest prawie zawsze wystarczająco dobre, ale w rzadkich przypadkach musisz określić, który okres na daną interakcję. Możesz to zrobić, dodając własne okno główne dopasowanie lub dopasowanie Root:

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

Tak jak w przypadku ViewMatchers, dostępny jest zestaw RootMatchers Zawsze możesz też zaimplementować własny obiekt Matcher.

Zapoznaj się z narzędziem MultipleWindowTest fragment w GitHubie.

Nagłówki i stopki są dodawane do ListViews za pomocą elementów addHeaderView() i addFooterView() metod. Aby mieć pewność, że usługa Espresso.onData() wie, który obiekt danych aby dopasować, przekaż gotową wartość obiektu danych jako drugi parametr. do: addHeaderView() i addFooterView(). Na przykład:

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

Następnie możesz napisać odpowiednik stopki:

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

A wczytanie widoku podczas testu jest proste:

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

   
// ...
}

Spójrz na pełny przykładowy kod znaleziony w metodzie testClickFooter() AdapterViewTest.java. w GitHubie.