В этой статье вы узнаете о том, как писать тесты пользовательского интерфейса с помощью инфраструктуры тестирования Espresso и автоматизировать рабочий процесс тестирования, вместо использования утомительного и подверженного ошибкам ручного процесса.
Espresso — это среда тестирования для написания тестов пользовательского интерфейса в Android. Согласно официальным документам, вы можете:
Используйте Espresso для написания кратких, красивых и надежных тестов пользовательского интерфейса Android.
1. Зачем использовать эспрессо?
Одна из проблем ручного тестирования заключается в том, что оно может быть трудоемким и утомительным. Например, чтобы проверить экран входа в систему (вручную) в приложении для Android, вам нужно будет сделать следующее:
- Запустите приложение.
- Перейдите к экрану входа.
- Убедитесь, что
usernameEditText
иpasswordEditText
видны. - Введите имя пользователя и пароль в соответствующие поля.
- Убедитесь, что кнопка входа также видна, а затем нажмите эту кнопку входа.
- Проверьте, отображаются ли правильные представления, когда этот вход был успешным или был неудачным.
Вместо того, чтобы тратить все это время на ручное тестирование нашего приложения, было бы лучше потратить больше времени на написание кода, который выделяет наше приложение среди остальных! И хотя ручное тестирование утомительно и довольно медленно, оно по-прежнему подвержено ошибкам, и вы можете пропустить некоторые крайние случаи.
Некоторые из преимуществ автоматизированного тестирования включают следующее:
- Автоматизированные тесты выполняют одни и те же тестовые случаи при каждом их выполнении.
- Разработчики могут быстро определить проблему до того, как она будет отправлена в команду QA.
- Это может сэкономить много времени, в отличие от ручного тестирования. Экономя время, инженеры-программисты и команда QA вместо этого могут тратить больше времени на сложные и полезные задачи.
- Более высокое тестовое покрытие достигается, что приводит к лучшему качеству применения.
В этом уроке мы узнаем об Espresso, интегрировав его в проект Android Studio. Мы напишем UI-тесты для экрана входа в систему и RecyclerView
и узнаем о целях тестирования.
Качество это не акт, это привычка. — Пабло Пикассо
2. Предпосылки
Чтобы следовать этому руководству, вам понадобится:
- базовое понимание основных API-интерфейсов Android и Kotlin
- Android Studio 3.1.3 или выше
- Плагин Kotlin 1.2.51 или выше
Пример проекта (на Kotlin) для этого учебного руководства можно найти в нашем репозитории GitHub, чтобы вы могли легко следить за ним.
3. Создайте проект Android Studio
MainActivity
Android Studio 3 и создайте новый проект с пустым действием MainActivity
. Обязательно установите флажок Включить поддержку Kotlin .
4. Настройте эспрессо и AndroidJUnitRunner
После создания нового проекта обязательно добавьте следующие зависимости из библиотеки поддержки тестирования Android в свой build.gradle (хотя Android Studio уже включила их для нас). В этом руководстве мы используем последнюю версию библиотеки Espresso 3.0.2 (на момент написания статьи).
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
android {
//…
defaultConfig {
//…
testInstrumentationRunner «android.support.test.runner.AndroidJUnitRunner»
}
//…
}
dependencies {
//…
androidTestImplementation ‘com.android.support.test.espresso:espresso-core:3.0.2’
androidTestImplementation ‘com.android.support.test:runner:1.0.2’
androidTestImplementation ‘com.android.support.test:rules:1.0.2’
}
|
Мы также включили инструментальный бегун AndroidJUnitRunner
:
Instrumentation
который запускает тесты JUnit3 и JUnit4 для пакета Android (приложения).
Обратите внимание, что Instrumentation
— это просто базовый класс для реализации кода инструментария приложения.
Отключить анимацию
Синхронизация Espresso, который не знает, как ждать завершения анимации, может привести к сбою некоторых тестов, если вы разрешите анимацию на своем тестовом устройстве. Чтобы отключить анимацию на тестовом устройстве, перейдите в « Настройки» > « Параметры разработчика» и отключите все следующие параметры в разделе «Рисование»:
- Масштаб оконной анимации
- Масштаб перехода анимации
- Шкала продолжительности аниматора
5. Напишите свой первый тест в эспрессо
Сначала мы начнем тестирование экрана входа в систему. Вот как начинается процесс входа в систему: пользователь запускает приложение, и первый показанный экран содержит одну кнопку входа в систему . При нажатии этой кнопки входа в систему открывается экран LoginActivity
в LoginActivity
. Этот экран содержит только два EditText
(поля имени пользователя и пароля) и кнопку « Отправить» .
Вот как выглядит наш макет MainActivity
:
Вот как выглядит наш макет LoginActivity
:
Давайте теперь напишем тест для нашего класса MainActivity
. Перейдите в свой класс MainActivity
, переместите курсор на имя MainActivity
и нажмите Shift-Control-T . Выберите Создать новый тест … во всплывающем меню.
Нажмите кнопку ОК , и появится другое диалоговое окно. Выберите каталог androidTest и нажмите кнопку ОК еще раз. Обратите внимание, что поскольку мы пишем тестирование инструментов (специальные тесты для Android SDK), тестовые примеры находятся в папке androidTest / java .
Теперь Android Studio успешно создала для нас тестовый класс. Над именем класса @RunWith(AndroidJUnit4::class)
эту аннотацию: @RunWith(AndroidJUnit4::class)
.
1
2
3
4
5
6
7
8
|
import android.support.test.runner.AndroidJUnit4
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class MainActivityTest {
}
|
Эта аннотация означает, что все тесты в этом классе являются специфичными для Android тестами.
Тестирование
Поскольку мы хотим протестировать действие, мы должны сделать небольшую настройку. Нам нужно сообщить Espresso, какую активность открыть или запустить перед выполнением и уничтожить после выполнения любого метода тестирования.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
import android.support.test.rule.ActivityTestRule
import android.support.test.runner.AndroidJUnit4
import org.junit.Rule
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class MainActivityTest {
@Rule @JvmField
var activityRule = ActivityTestRule<MainActivity>(
MainActivity::class.java
)
}
|
Обратите внимание, что аннотация @Rule
означает, что это тестовое правило JUnit4. Правила тестирования JUnit4 запускаются до и после каждого метода тестирования (с аннотацией @Test
). В нашем собственном сценарии мы хотим запускать MainActivity
перед каждым методом тестирования и уничтожать его после.
Мы также включили аннотацию @JvmField
Kotlin. Это просто указывает компилятору не генерировать геттеры и сеттеры для свойства, а вместо этого выставлять его как простое поле Java.
Вот три основных этапа написания теста для эспрессо:
- Найдите виджет (например,
TextView
илиButton
), который вы хотите протестировать. - Выполните одно или несколько действий с этим виджетом.
- Проверьте или проверьте, находится ли этот виджет в определенном состоянии.
Следующие типы аннотаций могут применяться к методам, используемым в тестовом классе.
-
@BeforeClass
: это указывает на то, что статический метод, к которому применяется эта аннотация, должен выполняться один раз и перед всеми тестами в классе. Это может быть использовано, например, для установки соединения с базой данных. -
@Before
: указывает, что метод, к которому прикреплена эта аннотация, должен выполняться перед каждым тестовым методом в классе. -
@Test
: указывает, что метод, к которому прикреплена эта аннотация, должен запускаться как контрольный пример. -
@After
: указывает, что метод, к которому прикреплена эта аннотация, должен запускаться после каждого метода тестирования. -
@AfterClass
: указывает, что метод, к которому прикреплена эта аннотация, должен запускаться после запуска всех методов тестирования в классе. Здесь мы обычно закрываем ресурсы, которые были открыты в@BeforeClass
.
Найти представление с помощью onView()
В нашем MainActivity
макета MainActivity
у нас есть только один виджет — кнопка « Login
. Давайте проверим сценарий, в котором пользователь найдет эту кнопку и нажмет на нее.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
import android.support.test.espresso.Espresso.onView
import android.support.test.espresso.matcher.ViewMatchers.withId
// …
@RunWith(AndroidJUnit4::class)
class MainActivityTest {
// …
@Test
@Throws(Exception::class)
fun clickLoginButton_opensLoginUi() {
onView(withId(R.id.btn_login))
}
}
|
Чтобы найти виджеты в Espresso, мы используем статический метод onView()
(вместо findViewById()
). Тип параметра, который мы предоставляем onView()
является Matcher
. Обратите внимание, что Matcher
API поступает не из Android SDK, а из проекта Hamcrest . Библиотека совпадений Hamcrest находится внутри библиотеки Espresso, которую мы вытащили через Gradle.
onView(withId(R.id.btn_login))
вернет ViewInteraction
который относится к представлению с идентификатором R.id.btn_login
. В приведенном выше примере мы использовали withId()
для поиска виджета с заданным идентификатором. Мы можем использовать и другие сопоставления представлений:
-
withText()
: возвращает сопоставление, совпадающее сTextView
на основе значения его свойства текста. -
withHint()
: возвращает сопоставление, совпадающее сTextView
на основе значения свойства подсказки. -
withTagKey()
: возвращает совпадение, которое соответствуетView
на основе ключей тега. -
withTagValue()
: возвращает сопоставление, которое соответствуетView
на основе значений свойства тега.
Сначала давайте проверим, действительно ли кнопка отображается на экране.
1
|
onView(withId(R.id.btn_login)).check(matches(isDisplayed()))
|
Здесь мы просто подтверждаем, R.id.btn_login
ли кнопка с указанным идентификатором ( R.id.btn_login
) пользователю, поэтому мы используем метод check()
чтобы подтвердить, имеет ли базовый View
определенное состояние — в нашем случае, если это видно.
Статический метод ViewAssertion
matches()
возвращает универсальный ViewAssertion
который утверждает, что представление существует в иерархии представлений и соответствует данному сопоставлению представлений. Это заданное сопоставление вида возвращается с помощью вызова isDisplayed()
. Как следует из названия метода, isDisplayed()
— это средство сопоставления, которое сопоставляет View
, которые в данный момент отображаются на экране, для пользователя. Например, если мы хотим проверить, включена ли кнопка, мы просто передаем isEnabled()
в matches()
.
Другие методы сопоставления популярных представлений, которые мы можем передать в метод match matches()
:
-
hasFocus()
: возвращает совпадение, которое соответствуетView
которые в данный момент имеют фокус. -
isChecked()
: возвращает совпадение, которое принимает, если и только если представление являетсяCompoundButton
(или подтипом) и находится в проверенном состоянии. Противоположностью этого метода являетсяisNotChecked()
. -
isSelected()
: возвращает сопоставление, которое соответствует выбраннымisSelected()
.
Чтобы запустить тест, вы можете нажать зеленый треугольник рядом с методом или именем класса. Нажатие на зеленый треугольник рядом с именем класса запустит все методы теста в этом классе, в то время как тот, что рядом с методом, запустит тест только для этого метода.
Ура! Наш тест пройден!
Выполнять действия в представлении
На объекте ViewInteraction
который возвращается с помощью onView()
, мы можем имитировать действия, которые пользователь может выполнять с виджетом. Например, мы можем смоделировать действие щелчка, просто вызвав статический метод click()
внутри класса ViewActions
. Это вернет ViewAction
объект ViewAction
.
В документации сказано, что ViewAction
это:
Отвечает за выполнение взаимодействия с данным элементом View.
1
2
3
4
5
6
|
@Test
fun clickLoginButton_opensLoginUi() {
// …
onView(withId(R.id.btn_login)).perform(click())
}
|
Мы выполняем событие click, сначала вызывая perform()
. Этот метод выполняет заданное действие (действия) на представлении, выбранном текущим сопоставителем представления. Обратите внимание, что мы можем передать ему одно действие или список действий (выполняемых по порядку). Здесь мы дали это click()
. Другие возможные действия:
-
typeText()
для имитации ввода текста вEditText
. -
clearText()
для имитации очистки текста вEditText
. -
doubleClick()
для имитации двойного щелчка поView
. -
longClick()
чтобы имитировать долгое нажатиеView
. -
scrollTo()
для имитации прокруткиScrollView
для определенного видимого представления. -
swipeLeft()
для имитации пролистывания справа налево через вертикальный центрView
.
В классе ViewActions
можно найти еще много симуляций.
Проверка с утверждениями представления
Давайте LoginActivity
наш тест, чтобы LoginActivity
экран LoginActivity
отображается при каждом нажатии кнопки входа . Хотя мы уже видели, как использовать check()
для ViewInteraction
, давайте использовать его снова, передав его другому ViewAssertion
.
1
2
3
4
5
6
|
@Test
fun clickLoginButton_opensLoginUi() {
// …
onView(withId(R.id.tv_login)).check(matches(isDisplayed()))
}
|
Внутри LoginActivity
макета LoginActivity
, кроме EditText
и Button
, у нас также есть TextView
с идентификатором R.id.tv_login
. Поэтому мы просто делаем проверку, чтобы подтвердить, что TextView
видим для пользователя.
Теперь вы можете запустить тест снова!
Ваши тесты должны пройти успешно, если вы правильно выполнили все шаги.
Вот что произошло во время выполнения наших тестов:
- Запустил
MainActivity
с помощью поляactivityRule
. - Проверено, была ли кнопка входа в систему (
R.id.btn_login
) видимой (isDisplayed()
) для пользователя. - Имитировал действие нажатия (
click()
) на эту кнопку. -
LoginActivity
была ли показанаLoginActivity
отображается лиTextView
с идентификаторомR.id.tv_login
вLoginActivity
.
Вы всегда можете обратиться к шпаргалке Espresso, чтобы увидеть различные сопоставления представлений, просмотреть действия и просмотреть доступные утверждения.
6. Протестируйте экран LoginActivity
в LoginActivity
Вот наш LoginActivity.kt :
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
class LoginActivity : AppCompatActivity() {
private lateinit var usernameEditText: EditText
private lateinit var loginTitleTextView: TextView
private lateinit var passwordEditText: EditText
private lateinit var submitButton: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
usernameEditText = findViewById(R.id.et_username)
passwordEditText = findViewById(R.id.et_password)
submitButton = findViewById(R.id.btn_submit)
loginTitleTextView = findViewById(R.id.tv_login)
submitButton.setOnClickListener {
if (usernameEditText.text.toString() == «chike» &&
passwordEditText.text.toString() == «password») {
loginTitleTextView.text = «Success»
} else {
loginTitleTextView.text = «Failure»
}
}
}
}
|
В приведенном выше коде, если введенное имя пользователя — «chike», а пароль — «пароль», то вход успешен. Для любого другого входа это сбой. Давайте теперь напишем тест эспрессо для этого!
Перейдите к LoginActivity.kt , переместите курсор на имя LoginActivity
и нажмите Shift-Control-T . Выберите Создать новый тест … во всплывающем меню. Выполните ту же процедуру, что и для MainActivity.kt , и нажмите кнопку ОК .
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
import android.support.test.espresso.Espresso
import android.support.test.espresso.Espresso.onView
import android.support.test.espresso.action.ViewActions
import android.support.test.espresso.assertion.ViewAssertions.matches
import android.support.test.espresso.matcher.ViewMatchers.withId
import android.support.test.espresso.matcher.ViewMatchers.withText
import android.support.test.rule.ActivityTestRule
import android.support.test.runner.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class LoginActivityTest {
@Rule
@JvmField
var activityRule = ActivityTestRule<LoginActivity>(
LoginActivity::class.java
)
private val username = «chike»
private val password = «password»
@Test
fun clickLoginButton_opensLoginUi() {
onView(withId(R.id.et_username)).perform(ViewActions.typeText(username))
onView(withId(R.id.et_password)).perform(ViewActions.typeText(password))
onView(withId(R.id.btn_submit)).perform(ViewActions.scrollTo(), ViewActions.click())
Espresso.onView(withId(R.id.tv_login))
.check(matches(withText(«Success»)))
}
}
|
Этот тестовый класс очень похож на наш первый. Если мы запустим тест, LoginActivity
наш экран LoginActivity
. Имя пользователя и пароль вводятся в поля R.id.et_username
и R.id.et_password
соответственно. Затем Espresso нажимает кнопку « Отправить» ( R.id.btn_submit
). Он будет ждать, пока не будет найден View
с идентификатором R.id.tv_login
с R.id.tv_login
чтением текста.
7. Протестируйте RecyclerView
RecyclerViewActions
— это класс, который предоставляет набор API для работы с RecyclerView
. RecyclerViewActions
является частью отдельного артефакта внутри артефакта espresso-contrib
, который также должен быть добавлен в build.gradle :
1
|
androidTestImplementation ‘com.android.support.test.espresso:espresso-contrib:3.0.2’
|
Обратите внимание, что этот артефакт также содержит API для пользовательского интерфейса, тестирующего DrawerActions
навигации с помощью DrawerActions
и DrawerMatchers
.
01
02
03
04
05
06
07
08
09
10
11
|
@RunWith(AndroidJUnit4::class)
class MyListActivityTest {
// …
@Test
fun clickItem() {
onView(withId(R.id.rv))
.perform(RecyclerViewActions
.actionOnItemAtPosition<RandomAdapter.ViewHolder>(0, ViewActions.click()))
}
}
|
Чтобы щелкнуть элемент в любой позиции в RecyclerView
, мы вызываем actionOnItemAtPosition()
. Мы должны дать ему тип предмета. В нашем случае элемент является классом ViewHolder
внутри нашего RandomAdapter
. Этот метод также принимает два параметра; первая — это позиция, а вторая — действие ( ViewActions.click()
).
Другие RecyclerViewActions
которые могут быть выполнены:
-
actionOnHolderItem()
: выполняетViewAction
для представления, соответствующегоviewHolderMatcher
. Это позволяет нам сопоставить его с тем, что содержится внутриViewHolder
а не с позицией. -
scrollToPosition()
: возвращаетViewAction
который прокручиваетRecyclerView
в позицию.
Далее (как только откроется «экран добавления заметки»), мы введем текст заметки и сохраним заметку. Нам не нужно ждать, пока откроется новый экран — Espresso сделает это автоматически для нас. Он ожидает, пока не будет найден вид с идентификатором R.id.add_note_title
.
8. Тестирование
Эспрессо использует другой артефакт, называемый espresso-intents
для проверки намерений. Этот артефакт является еще одним расширением для Эспрессо, которое фокусируется на проверке и насмешках над Интентами. Давайте посмотрим на пример.
Во-первых, мы должны espresso-intents
библиотеку espresso-intents
в наш проект.
1
|
androidTestImplementation ‘com.android.support.test.espresso:espresso-intents:3.0.2’
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
import android.support.test.espresso.intent.rule.IntentsTestRule
import android.support.test.runner.AndroidJUnit4
import org.junit.Rule
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class PickContactActivityTest {
@Rule
@JvmField
var intentRule = IntentsTestRule<PickContactActivity>(
PickContactActivity::class.java
)
}
|
IntentsTestRule
расширяет ActivityTestRule
, поэтому у них обоих одинаковое поведение. Вот что говорит доктор:
Этот класс является расширением
ActivityTestRule
, который инициализирует Espresso-Intents перед каждым тестом, помеченным с помощьюTest
и выпускает Espresso-Intents после каждого запуска теста. Деятельность будет прекращена после каждого теста, и это правило можно использовать так же, какActivityTestRule
.
Главная особенность заключается в том, что он имеет дополнительные функциональные возможности для тестирования startActivity()
и startActivityForResult()
с startActivityForResult()
и заглушками.
Теперь мы собираемся протестировать сценарий, в котором пользователь нажимает кнопку ( R.id.btn_select_contact
) на экране, чтобы выбрать контакт из списка контактов телефона.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
// …
@Test
fun stubPick() {
var result = Instrumentation.ActivityResult(Activity.RESULT_OK, Intent(null,
ContactsContract.Contacts.CONTENT_URI))
intending(hasAction(Intent.ACTION_PICK)).respondWith(result)
onView(withId(R.id.btn_select_contact)).perform(click())
intended(allOf(
toPackage(«com.google.android.contacts»),
hasAction(Intent.ACTION_PICK),
hasData(ContactsContract.Contacts.CONTENT_URI)))
//…
}
|
Здесь мы используем intending()
из библиотеки espresso-intents
чтобы настроить заглушку с ACTION_PICK
ответом на наш запрос ACTION_PICK
. Вот что происходит в PickContactActivity.kt, когда пользователь нажимает кнопку с идентификатором R.id.btn_select_contact
чтобы выбрать контакт.
1
2
3
4
5
|
fun pickContact(v: View)
val i = Intent(Intent.ACTION_PICK,
ContactsContract.Contacts.CONTENT_URI)
startActivityForResult(i, PICK_REQUEST)
}
|
intending()
принимает Matcher
который совпадает с намерениями, для которых должен быть предоставлен тупой ответ. Другими словами, Matcher
определяет, какой запрос вы заинтересованы в заглушке. В нашем случае мы используем hasAction()
(вспомогательный метод в IntentMatchers
), чтобы найти наш запрос ACTION_PICK
. Затем мы вызываем respondWith()
, который устанавливает результат для onActivityResult()
. В нашем случае результат имеет Activity.RESULT_OK
, имитирующий выбор пользователем контакта из списка.
Затем мы имитируем нажатие кнопки выбора контакта, которая вызывает startActivityForResult()
. Обратите внимание, что наша заглушка отправила ложный ответ на onActivityResult()
.
Наконец, мы используем вспомогательный метод intended()
чтобы просто проверить, что вызовы startActivity()
и startActivityForResult()
были сделаны с правильной информацией.
Вывод
Из этого руководства вы узнали, как легко использовать среду тестирования Espresso в проекте Android Studio для автоматизации рабочего процесса тестирования.
Я настоятельно рекомендую ознакомиться с официальной документацией, чтобы узнать больше о написании тестов пользовательского интерфейса с помощью Espresso.