Кофе с огурцами (Espresso + Cucumber)

в 9:35, , рубрики: android, bdd, cucumber, espresso, Разработка под android, Тестирование мобильных приложений

Кофе с огурцами (Espresso + Cucumber) - 1

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

Кофе с огурцами (Espresso + Cucumber) - 2

Изъятие отчетов с устройства

Если мы запустим тесты, они пройдут, но отчеты останутся лежать на устройстве. Значит по завершению тестирования их необходимо оттуда забрать. Идем в файл 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 для изъятия отчетов.

Кофе с огурцами (Espresso + Cucumber) - 3

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

Кофе с огурцами (Espresso + Cucumber) - 4
Запуск на 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.

На выходе мы получим вот такие отчеты:

Кофе с огурцами (Espresso + Cucumber) - 5

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

Кофе с огурцами (Espresso + Cucumber) - 6

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

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

Автор: Stabilitron

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js