Основы эспрессо

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

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

Компоненты API

К основным компонентам эспрессо относятся следующие:

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

Пример:

Котлин

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

Ява

// 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() принимает средство сопоставления подколенных ног, которое, как ожидается, будет соответствовать одному — и только одному — представлению в текущей иерархии представлений. Сопоставители — это мощные инструменты, которые будут знакомы тем, кто использовал их с Mockito или JUnit. Если вы не знакомы с приспособлениями для подколенных сухожилий, предлагаем вам начать с беглого ознакомления с этой презентацией .

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

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

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

Котлин

onView(withId(R.id.my_view))

Ява

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

Иногда значения R.id используются несколькими представлениями. В этом случае попытка использовать определенный R.id создает исключение, например AmbiguousViewMatcherException . Сообщение об исключении предоставляет вам текстовое представление текущей иерархии представлений, которую вы можете искать и находить представления, соответствующие неуникальному 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!" . Вы можете использовать это, чтобы сузить область поиска, используя средства сопоставления комбинаций:

Котлин

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

Ява

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

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

Котлин

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

Ява

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

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

Соображения

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

Выполнение действия над представлением

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

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

Котлин

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

Ява

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

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

Котлин

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

Ява

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

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

Котлин

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

Ява

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

См. ViewActions для просмотра действий, предоставляемых Espresso.

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

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

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

Котлин

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

Ява

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

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

Котлин

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

Ява

// 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 .

Котлин

onView(withId(R.id.button_simple))

Ява

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

Теперь, чтобы выполнить щелчок:

Котлин

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

Ява

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

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

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

Котлин

onView(withId(R.id.text_simple))

Ява

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

Теперь, чтобы проверить текст содержимого:

Котлин

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

Ява

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

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

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

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

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

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

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

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

Открыть выбор товара

Котлин

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

Ява

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

Выберите элемент

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

Котлин

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

Ява

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

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

Котлин

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

Ява

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 печатает иерархию представлений в сообщении об исключении при сбое onView() .

  • Если onView() не находит целевое представление, выдается исключение NoMatchingViewException . Вы можете проверить иерархию представлений в строке исключения, чтобы проанализировать, почему средство сопоставления не соответствует ни одному представлению.
  • Если onView() находит несколько представлений, соответствующих данному сопоставителю, генерируется исключение AmbiguousViewMatcherException . Иерархия представлений распечатывается, и все сопоставленные представления помечаются меткой 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****

При работе со сложной иерархией представлений или неожиданным поведением виджетов всегда полезно использовать для объяснения средство просмотра иерархии в Android Studio.

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

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

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

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

Образцы