Основы приготовления эспрессо

В этом документе объясняется, как выполнять распространенные задачи автоматизированного тестирования с помощью Espresso API.

Espresso API позволяет авторам тестов думать о том, что пользователь может делать при взаимодействии с приложением, например находить элементы интерфейса и взаимодействовать с ними. В то же время фреймворк предотвращает прямой доступ к действиям и представлениям приложения, поскольку удержание этих объектов и работа с ними вне потока UI – основная причина нестабильности тестов. Поэтому в Espresso API нет таких методов, как getView() и getCurrentActivity(). Вы можете безопасно работать с представлениями, создав собственные подклассы ViewAction и ViewAssertion.

Компоненты API

Основные компоненты Espresso:

  • Espresso – точка входа для взаимодействия с представлениями (через onView() и onData()). Также предоставляет API, которые не обязательно привязаны к какому-либо представлению, например pressBack().
  • ViewMatchers – набор объектов, реализующих интерфейс Matcher<? super View>. Вы можете передать один или несколько из них в метод onView(), чтобы найти представление в текущей иерархии представлений.
  • ViewActions – коллекция объектов ViewAction, которые можно передать методу ViewInteraction.perform(), например click().
  • ViewAssertions – набор объектов ViewAssertion, которые можно передать методу ViewInteraction.check(). В большинстве случаев вы будете использовать утверждение matches, которое использует сопоставитель View для утверждения состояния выбранного в данный момент представления.

Пример:

Kotlin

// withId(R.id.my_view) is a ViewMatcher
// click() is a ViewAction
// matches(isDisplayed()) is a ViewAssertion
onView(withId(R.id.my_view))
    .perform(click())
    .check(matches(isDisplayed()))

Java

// withId(R.id.my_view) is a ViewMatcher
// click() is a ViewAction
// matches(isDisplayed()) is a ViewAssertion
onView(withId(R.id.my_view))
    .perform(click())
    .check(matches(isDisplayed()));

Найти представление

В большинстве случаев метод onView() принимает сопоставитель Hamcrest, который должен соответствовать одному и только одному представлению в текущей иерархии представлений. Сопоставители – это мощный инструмент, который будет знаком тем, кто использовал их с Mockito или JUnit. Если вы не знакомы с сопоставителями Hamcrest, рекомендуем начать с этой презентации.

Часто у нужного представления есть уникальный идентификатор R.id, и простое сопоставление withId сузит поиск. Однако во многих случаях вы не можете определить R.id во время разработки теста. Например, у определенного представления может не быть R.id или R.id не уникален. Это может усложнить написание обычных инструментальных тестов, поскольку стандартный способ доступа к представлению (с помощью findViewById()) не работает. Таким образом, вам может потребоваться доступ к закрытым членам Activity или Fragment, содержащим представление, или найти контейнер с известным R.id и перейти к его содержимому для конкретного представления.

Espresso позволяет решить эту проблему, сузив область поиска с помощью существующих объектов ViewMatcher или собственных объектов.

Чтобы найти представление по его R.id, достаточно вызвать onView():

Kotlin

onView(withId(R.id.my_view))

Java

onView(withId(R.id.my_view));

Иногда значения R.id используются в нескольких представлениях. В этом случае при попытке использовать определенный R.id вы получите исключение, например AmbiguousViewMatcherException. В сообщении об исключении будет текстовое представление текущей иерархии View, в которой можно найти View, соответствующие неуникальному R.id:

java.lang.RuntimeException:
androidx.test.espresso.AmbiguousViewMatcherException
This matcher matches multiple views in the hierarchy: (withId: is <123456789>)

...

+----->SomeView{id=123456789, res-name=plus_one_standard_ann_button,
visibility=VISIBLE, width=523, height=48, has-focus=false, has-focusable=true,
window-focus=true, is-focused=false, is-focusable=false, enabled=true,
selected=false, is-layout-requested=false, text=,
root-is-layout-requested=false, x=0.0, y=625.0, child-count=1}
****MATCHES****
|
+------>OtherView{id=123456789, res-name=plus_one_standard_ann_button,
visibility=VISIBLE, width=523, height=48, has-focus=false, has-focusable=true,
window-focus=true, is-focused=false, is-focusable=true, enabled=true,
selected=false, is-layout-requested=false, text=Hello!,
root-is-layout-requested=false, x=0.0, y=0.0, child-count=1}
****MATCHES****

Просматривая различные атрибуты представлений, вы можете найти уникальные идентифицируемые свойства. В примере выше у одного из представлений есть текст "Hello!". Вы можете использовать это, чтобы сузить область поиска, используя комбинацию сопоставителей:

Kotlin

onView(allOf(withId(R.id.my_view), withText("Hello!")))

Java

onView(allOf(withId(R.id.my_view), withText("Hello!")));

Вы также можете не инвертировать сопоставители:

Kotlin

onView(allOf(withId(R.id.my_view), not(withText("Unwanted"))))

Java

onView(allOf(withId(R.id.my_view), not(withText("Unwanted"))));

Список сопоставителей представлений, доступных в Espresso, можно найти в ViewMatchers.

Что нужно учесть

  • В правильно настроенном приложении все представления, с которыми может взаимодействовать пользователь, должны содержать описательный текст или описание контента. Подробнее о том, как сделать приложения более доступными… Если вы не можете сузить область поиска с помощью withText() или withContentDescription(), считайте это ошибкой специальных возможностей.
  • Используйте наименее описательный сопоставитель, который находит нужное представление. Не указывайте слишком много параметров, чтобы не перегружать фреймворк. Например, если представление можно однозначно идентифицировать по тексту, не нужно указывать, что оно также может быть назначено из TextView. Для многих представлений достаточно R.id.
  • Если целевое представление находится внутри AdapterView, например ListView, GridView или Spinner, метод onView() может не работать. В таких случаях следует использовать onData().

Как выполнить действие в представлении

Найдя подходящий сопоставитель для целевого представления, вы можете выполнить для него экземпляры ViewAction с помощью метода perform.

Например, чтобы нажать на представление:

Kotlin

onView(...).perform(click())

Java

onView(...).perform(click());

Вы можете выполнить несколько действий с помощью одного вызова perform:

Kotlin

onView(...).perform(typeText("Hello"), click())

Java

onView(...).perform(typeText("Hello"), click());

Если представление, с которым вы работаете, находится внутри ScrollView (вертикального или горизонтального), перед действиями, требующими отображения представления, такими как click() и typeText(), рекомендуется использовать scrollTo(). Это гарантирует, что представление будет показано до перехода к другому действию:

Kotlin

onView(...).perform(scrollTo(), click())

Java

onView(...).perform(scrollTo(), click());

Список действий, доступных в Espresso, можно найти в ViewActions.

Проверка утверждений View

Утверждения можно применять к выбранному представлению с помощью метода check(). Чаще всего используется утверждение matches(). Для подтверждения состояния выбранного представления используется объект ViewMatcher.

Например, чтобы проверить, что в представлении есть текст "Hello!":

Kotlin

onView(...).check(matches(withText("Hello!")))

Java

onView(...).check(matches(withText("Hello!")));

Если вы хотите подтвердить, что "Hello!" – это контент представления, не рекомендуется делать следующее:

Kotlin

// Don't use assertions like withText inside onView.
onView(allOf(withId(...), withText("Hello!"))).check(matches(isDisplayed()))

Java

// Don't use assertions like withText inside onView.
onView(allOf(withId(...), withText("Hello!"))).check(matches(isDisplayed()));

С другой стороны, если вы хотите подтвердить, что представление с текстом "Hello!" видно, например после изменения флага видимости представлений, код в порядке.

Простой тест утверждения

В этом примере объект SimpleActivity содержит объекты Button и TextView. При нажатии кнопки содержимое TextView меняется на "Hello Espresso!".

Вот как это сделать с помощью Espresso:

Нажмите кнопку.

Сначала нужно найти свойство, которое поможет найти кнопку. Кнопка в SimpleActivity имеет уникальный R.id, как и ожидалось.

Kotlin

onView(withId(R.id.button_simple))

Java

onView(withId(R.id.button_simple));

Чтобы выполнить клик:

Kotlin

onView(withId(R.id.button_simple)).perform(click())

Java

onView(withId(R.id.button_simple)).perform(click());

Проверка текста TextView

Элемент TextView с текстом, который нужно проверить, также имеет уникальный атрибут R.id:

Kotlin

onView(withId(R.id.text_simple))

Java

onView(withId(R.id.text_simple));

Теперь проверим текст контента:

Kotlin

onView(withId(R.id.text_simple)).check(matches(withText("Hello Espresso!")))

Java

onView(withId(R.id.text_simple)).check(matches(withText("Hello Espresso!")));

Проверьте загрузку данных в представлениях адаптера

AdapterView – это особый тип виджета, который динамически загружает данные из адаптера. Самый распространенный пример AdapterView – ListView. В отличие от статических виджетов, таких как LinearLayout, в текущую иерархию View может быть загружено только подмножество дочерних элементов AdapterView. Простой поиск onView() не найдет представления, которые в данный момент не загружены.

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

Внимание! При использовании специальных реализаций AdapterView могут возникнуть проблемы с методом onData(), если они нарушают контракты наследования, в частности getItem() API. В таких случаях лучше всего провести рефакторинг кода приложения. Если это невозможно, вы можете реализовать собственный класс AdapterViewProtocol. Дополнительную информацию можно найти в описании класса AdapterViewProtocols, который по умолчанию предоставляется Espresso.

Простой тест представления адаптера

Этот простой тест демонстрирует, как использовать onData(). SimpleActivity содержит Spinner с несколькими элементами, представляющими типы кофейных напитков. Когда объект выбран, TextView меняется на "One %s a day!", где %s – выбранный объект.

Цель этого теста – открыть Spinner, выбрать определенный объект и убедиться, что он есть в TextView. Поскольку класс Spinner основан на AdapterView, для сопоставления объектов рекомендуется использовать onData() вместо onView().

Открыть выбор объектов

Kotlin

onView(withId(R.id.spinner_simple)).perform(click())

Java

onView(withId(R.id.spinner_simple)).perform(click());

Выберите объект

Для выбора элементов Spinner создает ListView с содержимым. Это представление может быть очень длинным, и элемент может не быть добавлен в иерархию представлений. Используя onData(), мы принудительно добавляем нужный элемент в иерархию View. Элементы в Spinner являются строками, поэтому мы хотим сопоставить элемент, равный строке "Americano":

Kotlin

onData(allOf(`is`(instanceOf(String::class.java)),
        `is`("Americano"))).perform(click())

Java

onData(allOf(is(instanceOf(String.class)), is("Americano"))).perform(click());

Проверьте правильность текста

Kotlin

onView(withId(R.id.spinnertext_simple))
    .check(matches(withText(containsString("Americano"))))

Java

onView(withId(R.id.spinnertext_simple))
    .check(matches(withText(containsString("Americano"))));

Отладка

Espresso предоставляет полезные сведения для отладки, когда тест не пройден:

Журналы

Espresso записывает все действия с представлениями в logcat. Пример:

ViewInteraction: Performing 'single click' action on view with text: Espresso

Просмотреть иерархию

Espresso выводит иерархию View в сообщении об исключении, когда onView() завершается неудачно.

  • Если onView() не находит целевое представление, выдается NoMatchingViewException. Вы можете изучить иерархию View в строке исключения, чтобы проанализировать, почему сопоставитель не нашел совпадений.
  • Если onView() находит несколько представлений, соответствующих заданному сопоставителю, выдается ошибка AmbiguousViewMatcherException. Будет напечатана иерархия View, и все совпавшие View будут помечены ярлыком MATCHES:
java.lang.RuntimeException:
androidx.test.espresso.AmbiguousViewMatcherException
This matcher matches multiple views in the hierarchy: (withId: is <123456789>)

...

+----->SomeView{id=123456789, res-name=plus_one_standard_ann_button,
visibility=VISIBLE, width=523, height=48, has-focus=false, has-focusable=true,
window-focus=true, is-focused=false, is-focusable=false, enabled=true,
selected=false, is-layout-requested=false, text=,
root-is-layout-requested=false, x=0.0, y=625.0, child-count=1}
****MATCHES****
|
+------>OtherView{id=123456789, res-name=plus_one_standard_ann_button,
visibility=VISIBLE, width=523, height=48, has-focus=false, has-focusable=true,
window-focus=true, is-focused=false, is-focusable=true, enabled=true,
selected=false, is-layout-requested=false, text=Hello!,
root-is-layout-requested=false, x=0.0, y=0.0, child-count=1}
****MATCHES****

Если вы столкнулись со сложной иерархией View или неожиданным поведением виджетов, используйте Hierarchy Viewer в Android Studio.

Предупреждения о представлении адаптера

Espresso предупреждает пользователей о наличии виджетов AdapterView. Если операция onView() вызывает ошибку NoMatchingViewException и в иерархии View есть виджеты AdapterView, чаще всего используется onData(). Сообщение об исключении будет содержать предупреждение со списком представлений адаптера. Вы можете использовать эту информацию, чтобы вызвать onData() для загрузки целевого представления.

Дополнительные ресурсы

Дополнительную информацию об использовании Espresso в тестах Android можно найти в следующих ресурсах.

Образцы