Рецепты эспрессо

В этом документе описывается, как настроить различные распространенные тесты эспрессо.

Сопоставление представления рядом с другим представлением

Макет может содержать определенные представления, которые сами по себе не являются уникальными. Например, кнопка повторяющегося вызова в таблице контактов может иметь тот же R.id , содержать тот же текст и те же свойства, что и другие кнопки вызова в иерархии представлений.

Например, в этом упражнении представление с текстом "7" повторяется в нескольких строках:

Действие списка, показывающее 3 копии одного и того же элемента представления внутри списка из трех элементов.

Часто неуникальное представление сочетается с какой-либо уникальной меткой, расположенной рядом с ним, например именем контакта рядом с кнопкой вызова. В этом случае вы можете использовать сопоставление hasSibling() , чтобы сузить выбор:

Котлин

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

Ява

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

Соответствие представлению, находящемуся внутри панели действий

ActionBarTestActivity имеет две разные панели действий: обычную панель действий и контекстную панель действий, создаваемую из меню параметров. Обе панели действий имеют один элемент, который всегда виден, и два элемента, которые видны только в меню переполнения. При щелчке по элементу TextView заменяется содержимым выбранного элемента.

Сопоставить видимые значки на обеих панелях действий очень просто, как показано в следующем фрагменте кода:

Котлин

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

Кнопка сохранения находится на панели действий в верхней части действия.

Код для контекстной панели действий выглядит идентично:

Котлин

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

Кнопка блокировки находится на панели действий в верхней части действия.

Нажатие на элементы в меню переполнения немного сложнее для обычной панели действий, поскольку на некоторых устройствах есть кнопка аппаратного меню переполнения, которая открывает переполненные элементы в меню параметров, а на некоторых устройствах есть кнопка программного меню переполнения, которая открывает обычное меню. меню переполнения. К счастью, Espresso справится с этим за нас.

Для обычной панели действий:

Котлин

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

Кнопка дополнительного меню видна, а под панелью действий в верхней части экрана отображается список.

Вот как это выглядит на устройствах с аппаратной кнопкой меню переполнения:

Кнопки дополнительного меню нет, а список отображается в нижней части экрана.

Для контекстной панели действий это снова очень просто:

Котлин

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

Кнопка дополнительного меню отображается на панели действий, а список параметров отображается под панелью действий, в верхней части экрана.

Чтобы просмотреть полный код этих примеров, просмотрите образец ActionBarTest.java на GitHub.

Утверждать, что представление не отображается

После выполнения ряда действий вам наверняка захочется подтвердить состояние тестируемого пользовательского интерфейса. Иногда это может быть негативный случай, например, когда что-то не происходит. Имейте в виду, что вы можете превратить любое средство сопоставления представлений hamcrest в ViewAssertion с помощью ViewAssertions.matches() .

В приведенном ниже примере мы берем сопоставитель isDisplayed() и инвертируем его, используя стандартный сопоставитель 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())));

Вышеупомянутый подход работает, если представление все еще является частью иерархии. Если это не так, вы получите NoMatchingViewException и вам нужно будет использовать ViewAssertions.doesNotExist() .

Утверждать, что представление отсутствует

Если представление исчезло из иерархии представлений (что может произойти, когда действие вызвало переход к другому действию), вам следует использовать 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());

Утвердить, что элемент данных отсутствует в адаптере

Чтобы доказать, что конкретный элемент данных не находится в AdapterView вам придется поступить немного по-другому. Нам нужно найти интересующий нас AdapterView и запросить хранящиеся в нем данные. Нам не нужно использовать onData() . Вместо этого мы используем onView() чтобы найти AdapterView , а затем используем другое средство сопоставления для работы с данными внутри представления.

Сначала сопоставитель:

Котлин

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

Тогда все, что нам нужно, это onView() чтобы найти 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")))));
    }
}

И у нас есть утверждение, которое потерпит неудачу, если элемент, равный «item: 168», существует в представлении адаптера со списком идентификаторов.

Полный пример см. в методе testDataItemNotInAdapter() в классе AdapterViewTest.java на GitHub.

Используйте собственный обработчик ошибок

Замена FailureHandler по умолчанию в Espresso на собственный позволяет выполнять дополнительную или другую обработку ошибок, например делать снимки экрана или передавать дополнительную отладочную информацию.

Пример CustomFailureHandlerTest демонстрирует, как реализовать собственный обработчик ошибок:

Котлин

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

Этот обработчик сбоев создает MySpecialException вместо NoMatchingViewException и делегирует все остальные сбои DefaultFailureHandler . CustomFailureHandler можно зарегистрировать в Espresso в методе setUp() теста:

Котлин

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

Для получения дополнительной информации см. интерфейс FailureHandler и Espresso.setFailureHandler() .

Целевые окна не по умолчанию

Android поддерживает несколько окон. Обычно это незаметно для пользователей и разработчика приложения, однако в некоторых случаях видны несколько окон, например, когда окно автозаполнения отображается поверх основного окна приложения в виджете поиска. Чтобы упростить задачу, по умолчанию Espresso использует эвристику, чтобы угадать, с каким Window вы собираетесь взаимодействовать. Эта эвристика почти всегда достаточно хороша; однако в редких случаях вам нужно будет указать, на какое окно должно быть нацелено взаимодействие. Вы можете сделать это, предоставив собственное средство сопоставления корневых окон или средство сопоставления 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());

Как и в случае с ViewMatchers , мы предоставляем набор предварительно предоставленных RootMatchers . Конечно, вы всегда можете реализовать свой собственный объект Matcher .

Взгляните на образец MultipleWindowTest на GitHub.

Верхние и нижние колонтитулы добавляются в ListViews с помощью методов addHeaderView() и addFooterView() . Чтобы гарантировать, что Espresso.onData() знает, какой объект данных соответствует, обязательно передайте заданное значение объекта данных в качестве второго параметра в addHeaderView() и addFooterView() . Например:

Котлин

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

Затем вы можете написать сопоставление для нижнего колонтитула:

Котлин

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

А загрузка представления в тесте тривиальна:

Котлин

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

    // ...
}

Взгляните на полный пример кода, который можно найти в методе testClickFooter() файла AdapterViewTest.java на GitHub.