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:
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:
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:
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")));
}
Kod wygląda identycznie w przypadku paska działań kontekstowych:
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")));
}
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ń:
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")));
}
Na urządzeniach z przyciskiem rozszerzonego menu sprzętowego ta strona wygląda tak:
W przypadku paska działań kontekstowych jest to bardzo proste:
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")));
}
}
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()
:
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()
:
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:
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
:
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:
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:
@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
:
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.
Dopasowanie nagłówka lub stopki w widoku listy
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:
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:
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:
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.