
Относительно не так давно появилась замечательная библиотека Espresso для тестирования UI Android приложений. Её преимущества над аналогами обозревались не один раз. Если вкратце, то они заключаются в том, что это разработка Google для собственной ОС (ранее они сами использовали Robotium), а так же в лаконичности синтаксиса и скорости работы. Итак, мы решили идти в ногу со временем и использовать Espresso. Но нам мало тех плюсов, что уже есть, мы хотим BDD (http://en.wikipedia.org/wiki/Behavior-driven_development), мы хотим скриншотов и отчетов в json и html, мы хотим запускать это все на CI, в конце концов! Но обо всем по порядку. Я расскажу как подружить Cucumber (http://habrahabr.ru/post/62958/) и Espresso (http://habrahabr.ru/post/212425/) на небольшом примере. Всех, кто устал от Appium, кто хочет уйти от Robotium и тех, кому небезразлично тестирование Android, прошу под кат.
Подключение
Мы будем использовать Gradle как средство сборки и разрешения зависимостей для нашего проекта. Очень рекомендую для тех, кто еще не видел, сайт http://gradleplease.appspot.com/. Сообщаем ему имя искомого модуля, а он возвращает строку для подключения ее в Gradle.
Создадим проект и подключим к нему Espresso и необходимые для нашей задачи модули Cucumber, для этого дополняем блок dependency, файла build.gradle следующим образом:
dependencies {
androidTestCompile('com.jakewharton.espresso:espresso-support-v4:1.1-r3')
androidTestCompile 'info.cukes:cucumber-core:1.1.8'
androidTestCompile 'info.cukes:cucumber-java:1.1.8'
androidTestCompile 'info.cukes:cucumber-html:0.2.3'
androidTestCompile ('info.cukes:cucumber-android:1.2.2')
androidTestCompile ('info.cukes:cucumber-junit:1.1.8')
{
exclude group: 'org.hamcrest', module: 'hamcrest-core'
exclude group: 'org.hamcrest', module: 'hamcrest-integration'
exclude group: 'org.hamcrest', module: 'hamcrest-library'
}
}
Для того, чтобы мы могли использовать средства Espresso, нам необходимо, чтобы тесты запускались через GoogleInstrumentationTestRunner. Значит для подключения Cucumber нужно наследоваться от этого класса, внутри которого мы передад��м ему все управление.
public class CucuRunner extends GoogleInstrumentationTestRunner{
private CucumberInstrumentationCore helper;
public CucuRunner() {
helper = new CucumberInstrumentationCore(this);
}
@Override
public void onCreate(Bundle arguments) {
helper.create(arguments);
super.onCreate(arguments);
}
@Override
public void onStart() {
helper.start();
}
}
Не забываем указать наш свежесозданный instrumentation test runner в build.gradle
defaultConfig {
...
testInstrumentationRunner 'habrahabr.ru.myapplication.test.CucuRunner'
...
}
Шаги (Steps)
Теперь нам необходимо создать шаги, которые будут использоваться в наших тестовых сценариях. В нашем случае ими будут небольшие тесты, объединенные в один кейс. Для этого создаем соответствующий класс, который наследуем от стандартного для Espresso набора тестов, дабы иметь доступ ко всем необходимым вещам. Добавляем к этому классу аннотацию, где указываем, что это тесты Cucumber, а результат их работы следует поместить в отчеты соответствующих форматов, в нужные нам директории. Обратите внимание, Espresso-тесты исполняются на устройстве, и поэтому доступа к директориям компьютера у нас нет. Значит складываем все в директорию нашего приложения:
@CucumberOptions(format = {"pretty","html:/data/data/habrahabr.ru.myapplication/html", "json:/data/data/habrahabr.ru.myapplication/jreport"},features = "features")
public class CucumberActivitySteps extends ActivityInstrumentationTestCase2<MainActivity> {
Теперь можем заняться непосредственно реализацией шагов. Для этого необходимо разделить методы по их назначению в соответствии с BDD, то есть на Given, When и Then. Для этого используются аннотации, содержащие строку для нахождения соответствий в файле сценария на основе регулярных выражений, группы в которых играют роль входных аргументов, а в теле самих шагов мы будем использовать вызовы Espresso:
@Given("^Счетчик попыток входа показывает (\\d)$")
public void givenLoginTryCounter(Integer counterValue) {
String checkString = String.format(getActivity().getResources().getString(R.string.login_try_left), counterValue);
onView(withId(R.id.lblCounter)).check(matches(withText(checkString)));
}
@When("^Пользователь нажимает кнопку назад$")
public void clickOnBackButton() {
ViewActions.pressBack();
}
@When("^Пользователь '(.+)' авторизуется в системе с паролем '(.+)'$")
public void userLogin(String login, String password) {
onView(withId(R.id.txtUsername)).perform(ViewActions.clearText());
onView(withId(R.id.txtPassword)).perform(ViewActions.clearText());
onView(withId(R.id.txtUsername)).perform(ViewActions.typeText(login));
onView(withId(R.id.txtPassword)).perform(ViewActions.typeText(password));
onView(withId(R.id.btnLogin)).perform(ViewActions.click());
}
@Then("^Счетчик попыток входа должен показывать (\\d)$")
public void checkLoginTryCounter(Integer counterValue) {
givenLoginTryCounter(counterValue);
}
@Then("^Кнопка входа стала неактивной$")
public void checkLoginButtonDisabled() {
onView(withId(R.id.btnLogin)).check(matches(not(isEnabled())));
}
Скриншоты
В случае неудачного завершения последнего шага мы будем снимать скриншот и добавлять полученное изображение к отчету. Все остальное за нас сделает Cucumber, и мы сможем увидеть состояние экрана в момент ошибки. Приведенный далее способ не подходит, например, для снятия снимка с диалогом, но это тема для отдельного разговора.
@After
public void embedScreenshot(Scenario scenario) {
if(scenario.isFailed()) {
Bitmap bitmap;
final Activity activity = getActivity();
View view = getActivity().getWindow().getDecorView();
view.setDrawingCacheEnabled(true);
bitmap = Bitmap.createBitmap(view.getDrawingCache());
view.setDrawingCacheEnabled(false);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
scenario.embed(stream.toByteArray(), "image/png");
}
}
Сценарии
Осталась самая приятная часть. Имея в руках шаги, описанные в CucumberActivitySteps, мы можем написать сами тесты на человеческом языке, который будет доступен не только разработчикам, но и всем другим заинтересованным лицам:
Feature: Авторизация
Scenario: Пользователь пытается авторизоваться, используя неверные логин и пароль
Given Счетчик попыток входа показывает 3
When Пользователь 'RandomName' авторизуется в системе с паролем 'wrongPassword'
Then Счетчик попыток входа должен показывать 2
And Появилось сообщение 'Неверное имя пользователя или пароль.'
Эти сценарии мы сохраняем в директорию features, в которой наш исполняющий класс будет их искать (см. аннотацию CucumberOptions).

Изъятие отчетов с устройства
Если мы запустим тесты, они пройдут, но отчеты останутся лежать на устройстве. Значит по завершению тестирования их необходимо оттуда забрать. Идем в файл build.gradle и пишем соответствующий task, который посредством утилиты adb и команды pull скопирует файлы отчетов в заданную директорию.
task afterTests(type: Exec, dependsOn:runCucuTests) {
commandLine "${android.sdkDirectory}" + "/platform-tools/adb", 'pull', '/data/data/habrahabr.ru.myapplication/html', System.getProperty("user.dir") + "/cucumber_reports"
}
Теперь можно все запустить через IDE, нужно лишь создать соответствующую конфигурацию запуска, а после завершения тестов исполнить наш task для изъятия отчетов.

Отчеты же будут сохранены в указанную выше директорию

Запуск на CI
Но мы не хотим запускать тесты через IDE, мы хотим запускать их из консоли, а connectedCheck нам не подходит. Значит пишем новый task. И здесь мы, к сожалению, не придумали ничего лучше, чем собирать приложение и устанавливать его на устройство, после чего отсылать команду на старт тестирования через adb. А после всего этого забирать отчеты описанным выше task'ом.
task runCucuTests(type: Exec, dependsOn:'installDebugTest'){
commandLine "${android.sdkDirectory}" + "/platform-tools/adb", 'shell', 'am', 'instrument', '-w', 'habrahabr.ru.myapplication.test/.CucuRunner', 'echo', 'off'
finalizedBy('afterTests')
}
В принципе этого уже достаточно, чтобы запустить тесты на CI.
На выходе мы получим вот такие отчеты:

И к каждому сценарию, который завершился неудачно, у нас будет приложен скриншот:

На этом, пожалуй, и остановимся. Здесь многое еще хочется улучшить, например, получить нормальный output в консоль во время выполнения тестов, содержащий информацию о прогрессе, хочется сделать файл с отчетами красивым и многое другое. Надеюсь будет еще такая возможность. Для всех заинтересованных сам проект выложен на Github: https://github.com/Stabilitron/espresso-cucumber-example
Спасибо за внимание. Стабильных вам релизов!
