Относительно не так давно появилась замечательная библиотека 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

Спасибо за внимание. Стабильных вам релизов!

Комментарии (2)


  1. Ghedeon
    23.04.2015 17:11
    +2

    Ни одного комментария за сутки — хороший показатель того, как много людей тестируют на андроид).

    «Огурец» ­— одна из тех немногих вещей, от которой сразу начинаешь получать удовольствие, как только на нее сядешь. Внедрили в наш проект около двух лет назад и обрели силу земли.

    connectedCheck отлично работает, можно посмотреть как это сделано в официальном примере. Последний коммит от парня из моей команды: один из основных контрибьюторов в cucumber for android. Если унаследоваться от GoogleInstrumentation и прописать его в testInstrumentationRunner, то все должно работать из коробки. Другое дело, что нет поддержки параметров, но на этот тухес тоже есть свой элегантный болт: прокидываем gradle параметры в BuildConfig, а оттуда в Bundle, который у вас в CucuRunner. Мой ответ на stackoverflow, как запускать по тэгу. Запуск по имени сценария и по feature файлу слегка другой, но суть та же. Приятный бонус — отпадет необходимость собирать отчеты вручную, gradle все сложит в build/outputs/reports/..., а плагин для Jenkins подхватит index.html и прикрепит к тесту.

    Непаханое поле, на самом деле: улучшить вывод в консоль (quick & dirty: добавить -i к запуску); сделать dispatcher runner, который будет поддерживать cucumber runner не в ущерб классическому, чтобы не пришлось отказываться от интеграционных тестов; сделать форк cucumber плагина для IntelliJ, чтобы запускать конкретный тест из IDE (а не все сразу, как сейчас), и тд.

    Статью одобряю! Всем зеленых тестов без пупырышек.


  1. kaftanati
    24.04.2015 13:58

    Ни одного комментария за сутки — хороший показатель того, как много людей тестируют на андроид).
    Судя по количеству добавления в «Избранное» — все может измениться.

    Как мне кажется, самостоятельные разработчики, после почти принудительного перехода на Android Studio из Eclipse, уже имеют больше причин использовать автотесты из под Gradle.