Przepisy na espresso

W tym dokumencie opisujemy, jak skonfigurować różne często używane testy espresso.

Dopasowywanie widoku obok innego widoku

Układ może zawierać określone widoki, które nie są niepowtarzalne. Na przykład przycisk cyklicznego połączenia w tabeli kontaktów może mieć taki sam element R.id, zawierać ten sam tekst i mieć te same właściwości co inne przyciski wywołania w hierarchii widoków.

Na przykład w tym ćwiczeniu widok z tekstem "7" powtarza się w wielu wierszach:

Aktywność listy pokazująca 3 kopie tego samego widoku w ramach listy zawierającej 3 elementy

Nieunikalny widok jest często powiązany z jakąś unikalną etykietą, która znajduje się obok niego, np. nazwą kontaktu obok przycisku połączenia. W takim przypadku możesz użyć dopasowania hasSibling(), aby zawęzić wybór:

Kotlin

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

Java

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

Dopasowywanie widoku na pasku działań

ActionBarTestActivity ma 2 paski działań: zwykły pasek działań i pasek działań kontekstowych, który możesz utworzyć w menu opcji. Oba paski działań zawierają 1 element, który jest zawsze widoczny, i 2 elementy, które są widoczne tylko w rozszerzonym menu. Kliknięcie elementu powoduje zmianę pola TextView na zawartość klikniętego elementu.

Dopasowywanie widocznych ikon na obu paskach działań jest proste, co pokazuje ten fragment kodu:

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

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

Kod wygląda tak samo na pasku działań kontekstowych:

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

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

Klikanie elementów w rozszerzonym menu jest trochę trudniejsze na normalnym pasku działań, ponieważ niektóre urządzenia mają sprzętowy przycisk rozszerzonego menu, który otwiera przewijane elementy w menu opcji, a na niektórych urządzeniach jest przycisk oprogramowania, którego menu rozszerzone się otwiera. Na szczęście Espresso robi to za nas.

Na normalnym pasku działań:

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

Przycisk rozszerzonego menu jest widoczny, a pod paskiem działań u góry ekranu pojawia się lista.

Tak to wygląda na urządzeniach ze sprzętowym przyciskiem rozszerzonego menu:

Nie ma przycisku rozszerzonego menu, a u dołu ekranu pojawia się lista.

Ponowne uruchamianie paska działań kontekstowych jest bardzo proste:

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

Przycisk rozszerzonego menu pojawi się na pasku działań, a lista opcji pojawi się pod paskiem działań, u góry ekranu

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

Twierdzenie, że widok nie jest wyświetlany

Po wykonaniu serii działań warto potwierdzić stan testowanego interfejsu użytkownika. Czasem może to być niekorzystne, np. gdy coś się nie dzieje. Pamiętaj, że możesz przekształcić dowolne dopasowanie widoku hamcrest w ViewAssertion, używając funkcji ViewAssertions.matches().

W poniższym przykładzie wybieramy dopasowanie isDisplayed() i odwracamy je, używając standardowego dopasowania not():

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

Powyższe podejście działa, jeśli widok jest nadal częścią hierarchii. Jeśli nie, otrzymasz NoMatchingViewException. Musisz użyć aplikacji ViewAssertions.doesNotExist().

Twierdzenie o braku widoku

Jeśli widok danych zniknie z hierarchii widoków – co może się zdarzyć, gdy działanie spowodowało przejście do innej aktywności – użyj 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());

Twierdzić, że elementu danych nie ma w adapterze.

Aby udowodnić, że określony element danych nie znajduje się w elemencie AdapterView, musisz zrobić to trochę inaczej. Musimy znaleźć interesującą Cię usługę AdapterView i przeanalizować dane, które się w niej znajdują. Nie musimy używać usługi onData(). Zamiast tego używamy onView(), aby znaleźć AdapterView, a następnie używamy innego dopasowania, aby pracować z danymi w widoku.

Najpierw dopasowanie:

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

Następnie wystarczy onView(), aby znaleźć: 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")))));
    }
}

Mamy też asercję, która nie powiedzie się, jeśli element o wartości „item: 168” występuje w widoku adaptera z listą identyfikatorów.

Pełny przykład znajdziesz w metodzie testDataItemNotInAdapter() w klasie AdapterViewTest.java na GitHubie.

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

Zastąpienie domyślnego FailureHandler w Espresso adresem niestandardowym pozwala na dodatkową lub inną obsługę błędów, takich jak robienie zrzutu ekranu lub przekazywanie dodatkowych informacji na potrzeby debugowania.

Przykład CustomFailureHandlerTest pokazuje, jak wdrożyć niestandardowy moduł obsługi awarii:

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

Ten moduł obsługi błędów zgłasza MySpecialException zamiast NoMatchingViewException i przekazuje wszystkie inne błędy do DefaultFailureHandler. CustomFailureHandler można zarejestrować w Espresso w metodzie setUp() testu:

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

Więcej informacji znajdziesz w interfejsie FailureHandler i Espresso.setFailureHandler().

Kierowanie na okna inne niż domyślne

Android obsługuje wiele okien. Zwykle jest to widoczne dla użytkowników i dewelopera aplikacji, ale w niektórych przypadkach widocznych jest wiele okien, na przykład po przeciągnięciu okna autouzupełniania nad głównym oknem aplikacji w widżecie wyszukiwania. Aby uprościć sprawę, domyślnie Espresso korzysta z metody heurystycznej do odgadnięcia, z którym elementem Window chcesz wejść w interakcję. Taka heurystyka prawie zawsze jest wystarczająca, ale w rzadkich przypadkach trzeba określić okno, na które ma być kierowana interakcja. Możesz to zrobić, podając własny mechanizm dopasowywania okna głównego lub dopasowanie 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());

Tak jak w przypadku ViewMatchers, udostępniamy zestaw wstępnie przygotowanych RootMatchers. Oczywiście zawsze możesz zaimplementować własny obiekt Matcher.

Zapoznaj się z przykładowym testem MultipleWindowTest na GitHubie.

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

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

Następnie możesz zastosować dopasowanie stopki:

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

Wczytywanie widoku w teście jest proste:

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

    // ...
}

Zapoznaj się z pełnym przykładem kodu dostępnym w metodzie testClickFooter() w AdapterViewTest.java na GitHubie.