Автоматическое тестирование в Android

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

Для тестирования Android-приложений существует несколько сторонних фреймворков — Robotium, Robolectric и т.д., и Espresso, который является частью Android Support Library. Мы начали писать тесты на Robotium, но впоследствии отказались от него в пользу Espresso, так как у последнего есть несколько преимуществ:

  • Синхронизация с UI потоком. Так как тесты выполняется в отдельном потоке, без синхронизации с UI они могут быть нестабильными. Например тест может предпринять попытку нажать на кнопку до того, как она будет отрисована, так как рендеринг интерфейса занимает некоторое время.  Отсутствия синхронизации тестов с UI-потоками было головной болью при использовании Robotium, т.к. часто приходилось останавливать поток теста с помощью Thread.sleep(long). У Espresso-тестов такой проблемы нет.
  • Более простой и легко расширяемый API. Например, если не хватает каких-то view action’ов или view matcher’ов, можно написать свои.
  • Информативные сообщения о причинах невыполнения теста. Espresso покажет view hierarchy и сообщит, почему не выполнился какой-то view action или check.

Простые Espresso-тесты

Espresso имеет т.н. “формулу” для описания действия пользователя:

Espresso.onView(Matcher<View>).perform(ViewAction).check(ViewAssertion);

Т.e. метод onView(ViewMatcher) возвращает объект класса ViewInteraction, над которым нужно выполнить какое-то действие и/или как-то проверить его состояние. Для примера, протестируем launcher activity нашего приложения Mobilkassan. Вёрстка launcher activity состоит из ImageView (R.id.splash), при нажатии на который пользователь попадает в другое activity.

image02
Добавьте необходимые зависимости в build.gradle:

dependencies {

androidTestCompile ‘com.android.support.test:runner:0.4’
androidTestCompile ‘com.android.support.test:rules:0.4’
androidTestCompile ‘com.android.support.test.espresso:espresso-core:2.2.1’
}

Создайте класс LoginTest в директории src/androidTest/java:

import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.withId;

@RunWith(AndroidJUnit4.class)
public class LoginTest extends Pos4MobileTest {

@Rule
public ActivityTestRule<CashRegisterStartupActivity> rule = new ActivityTestRule<>(CashRegisterStartupActivity.class)

@Test
public void testTapOnMainScreen() throws Exception {
onView(withId(R.id.splash)).perform(click());
onView(withId(R.id.Main_ViewPager)).check(matches(isDisplayed()));

}

}

В аннтоации @RunWith указывается test runner (можно создать свой). Для указания, какую activity нужно запускать для тестов, используется объект класса ActivityTestRule и аннотация @Rule, и это не обязательно должна быть launcher activity, можно указать любую. Сам код теста находится в методе, помеченным аннотацией @Test.

Espresso находит View — ViewMatchers.withId(R.id.splash), и выполняет над ним действие — ViewActions.click(). Затем проверяется результат: Espresso находит View — ViewMatchers.withId(R.id.Main_ViewPager), и проверяет — ViewAssertions.matches(Matcher<View>), что он удовлетворяет условию — ViewMatchers.isDisplayed(). Искать view можно не только по id, но и по многим другим критериям (withText, withParent, withChild и т.д.), а также комбинировать с помощью методов Matchers.allOf(Matcher…) и Matchers.anyOf(Matcher…) из пакета org.hamcrest.

Методы perform и check принимают на вход varargs, то есть можно перечислить несколько action’ов и assert’ов соответственно, например:

perform(scrollTo(), click());

С action’ом scrollTo() есть один ньюанс: если целевой View находится в ScrollView, у которого есть padding, то scrollTo() выполнится без учета отступов и целевой View может оказатся (частично) невидим для пользователя. Для решения этой проблемы padding нужно перенести на 1 уровень вниз, в child ViewGroup этого ScrollView.

Так же объекту ViewInteraction (который возвращается методом Espresso.onView(Matcher<View>)) можно указать Root Matcher. Это может быть полезно для тестирования Toast сообщений: они используют non-default Window, поэтому обычный поиск по тексту или id не сработает.

У Window Toast сообщений есть параметр — WindowManager.LayoutParams.TYPE_TOAST — который отличает его от default Window (в котором находится верстка текущего activity). Этим можно воспользоваться, чтобы создать свой Root Matcher. В нем необходимо переопределить метод matchesSafely(), создав нужный критерий поиска (он описывается в методе describeTo()). Если Matcher не найдет подходящий под его критетий объект, текст описания добавится к сообщению об ошибке. Так можно реализовать свои Matcher’ы и ViewAction’ы в случае, если не хватает имеющихся.

Запуск тестов в Android Studio

Запустить тесты можно:

  • нажав правой кнопкой мыши на классе и выбрав Run ‘Test Class Name’ (будут выполнены все тесты этого класса):

    image08

  • нажав правой кнопкой мыши на конкретном тесте и выбрав Run ‘test Method Name’:

    image06

  • выполнив в терминале комманду gradle connectedCheck, в таком случае будут выполнены все тесты.

Если тест выполнился успешно, то вы увидите подобный экран:

image05

Если по каким-то причинам тест не выполнился, то вы увидите сообщение об ошибке:

image01

Слева и справа есть список проваленных тестов, по нажатию на них Android Studio покажет метод этого теста.

После выполнения gradle connectedCheck будет сгенерирован отчет, посмотреть его можно, открыв файл …/build/reports/androidTests/connected/flavors/dev/index.html

image00

Нажав на имя класса или метода неудачного теста можно посмотреть причины провала и View Hierarchy на этот момент:

image07

Для запуска тестов на нескольких устройствах одновременно можно использовать библиотеку Spoon. Так же существуют облачные сервисы, которые продают время реальных мобильных устройств для запуска тестов:

Продвинутое тестирование

Тестирование ListView, GridView и им подобных, то есть наследников AdapterView, немного отличается, “формула” принимает вид:

Espresso.onData(Matcher<Object>).DataOptions.perform(ViewAction).check(ViewAssertion);

Это связано с тем, что AdapterView состоит из нескольких практически одинаковых View, возвращаемых методом getView() адаптера. Так же объект класса DataInteraction (его возвращает метод onData(Matcher<>)) сам выполнит scroll списка к его item’у.

Так как большинство приложений активно взамодействуют с серверами, возникают две проблемы: как тестировать, если сервер в данный момент недоступен, и как тестировать сообщения об ошибках (например, сообщения о недоступности сервера или о некорректных данных от сервера)? Для решения этих проблем используют Mock-объекты в качестве ответов от сервера. То есть во время тестирования реального взаимодействия с сервером нет, а нужные для конкретного теста данные от сервера, либо ошибки сетевого уровня, симулируются. Для этого используется фреймворк Mockito. Так же необходимо использовать паттерн Dependency Injection, реализовать который удобно с помощью фреймворка Dagger2.