Статьи

Тестирование пользовательских интерфейсов Android с эспрессо

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

Espresso — это среда тестирования для написания тестов пользовательского интерфейса в Android. Согласно официальным документам, вы можете:

Используйте Espresso для написания кратких, красивых и надежных тестов пользовательского интерфейса Android.

Одна из проблем ручного тестирования заключается в том, что оно может быть трудоемким и утомительным. Например, чтобы проверить экран входа в систему (вручную) в приложении для Android, вам нужно будет сделать следующее:

  1. Запустите приложение.
  2. Перейдите к экрану входа.
  3. Убедитесь, что usernameEditText и passwordEditText видны.
  4. Введите имя пользователя и пароль в соответствующие поля.
  5. Убедитесь, что кнопка входа также видна, а затем нажмите эту кнопку входа.
  6. Проверьте, отображаются ли правильные представления, когда этот вход был успешным или был неудачным.

Вместо того, чтобы тратить все это время на ручное тестирование нашего приложения, было бы лучше потратить больше времени на написание кода, который выделяет наше приложение среди остальных! И хотя ручное тестирование утомительно и довольно медленно, оно по-прежнему подвержено ошибкам, и вы можете пропустить некоторые крайние случаи.

Некоторые из преимуществ автоматизированного тестирования включают следующее:

  • Автоматизированные тесты выполняют одни и те же тестовые случаи при каждом их выполнении.
  • Разработчики могут быстро определить проблему до того, как она будет отправлена ​​в команду QA.
  • Это может сэкономить много времени, в отличие от ручного тестирования. Экономя время, инженеры-программисты и команда QA вместо этого могут тратить больше времени на сложные и полезные задачи.
  • Более высокое тестовое покрытие достигается, что приводит к лучшему качеству применения.

В этом уроке мы узнаем об Espresso, интегрировав его в проект Android Studio. Мы напишем UI-тесты для экрана входа в систему и RecyclerView и узнаем о целях тестирования.

Качество это не акт, это привычка. — Пабло Пикассо

Чтобы следовать этому руководству, вам понадобится:

Пример проекта (на Kotlin) для этого учебного руководства можно найти в нашем репозитории GitHub, чтобы вы могли легко следить за ним.

MainActivity Android Studio 3 и создайте новый проект с пустым действием MainActivity . Обязательно установите флажок Включить поддержку Kotlin .

Диалог создания проекта Android

После создания нового проекта обязательно добавьте следующие зависимости из библиотеки поддержки тестирования 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, который не знает, как ждать завершения анимации, может привести к сбою некоторых тестов, если вы разрешите анимацию на своем тестовом устройстве. Чтобы отключить анимацию на тестовом устройстве, перейдите в « Настройки» > « Параметры разработчика» и отключите все следующие параметры в разделе «Рисование»:

  • Масштаб оконной анимации
  • Масштаб перехода анимации
  • Шкала продолжительности аниматора

Сначала мы начнем тестирование экрана входа в систему. Вот как начинается процесс входа в систему: пользователь запускает приложение, и первый показанный экран содержит одну кнопку входа в систему . При нажатии этой кнопки входа в систему открывается экран LoginActivity в LoginActivity . Этот экран содержит только два EditText (поля имени пользователя и пароля) и кнопку « Отправить» .

Вот как выглядит наш макет MainActivity :

MainActivity layout

Вот как выглядит наш макет 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 .

В нашем 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() .

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

MainActivityTest класс

Ура! Наш тест пройден!

Панель инструментов результатов теста Android Studio

На объекте 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 видим для пользователя.

Теперь вы можете запустить тест снова!

Панель инструментов результатов теста Android Studio

Ваши тесты должны пройти успешно, если вы правильно выполнили все шаги.

Вот что произошло во время выполнения наших тестов:

  1. Запустил MainActivity с помощью поля activityRule .
  2. Проверено, была ли кнопка входа в систему ( R.id.btn_login ) видимой ( isDisplayed() ) для пользователя.
  3. Имитировал действие нажатия ( click() ) на эту кнопку.
  4. LoginActivity была ли показана LoginActivity отображается ли TextView с идентификатором R.id.tv_login в LoginActivity .

Вы всегда можете обратиться к шпаргалке Espresso, чтобы увидеть различные сопоставления представлений, просмотреть действия и просмотреть доступные утверждения.

Вот наш 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 чтением текста.

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 .

Эспрессо использует другой артефакт, называемый 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.