Часть 1


Сегодня поговорим о создании UI smoke-теста для сайта с использованием фреймворков Cucumber и Selenide. Статья рассчитана на junior, который совсем ничего не знает про данные фреймворки. Опытный junior найдет во второй части интересные моменты, до которых я доходил пару месяцев.
Статья состоит из двух частей:


  • в первой описано создание нашего теста простейшим способом – чтобы запускалось и при этом никаких сложных вещей из фреймворков не использовалось. Только создадим описание фичи (.feature файл) и класс описания степов с использованием Selenide.
  • во второй части в тот же самый тест добавим всякие интересные штуки от Selenide, посмотрим, как создавать красивые отчеты, которые будут содержать текст фич (мн.ч от слова «фича»).

Фреймворки


Selenide – фреймворк (а точнее библиотека), обертывающий Selenium. Чем он отличается, прекрасно описано автором, Андреем Солнцевым. Главное отличие – Selenide позволяет сократить кучу строчек кода при написании UI тестов, что является одной из главных задач при создании тестов/написании кода, ибо Вы должны заботиться о том тестере, который придет после Вас и должен будет разбирать Ваше творение.


Cucumber – это фреймворк, реализующий подход BDD/TDD.



Я не претендую на глубокое теоретическое знание BDD/TDD, пока что для меня они суть одно и тоже.


BDD/TDD с практической точки зрения:


  1. От бизнеса приходит тех. задание, на основании которого программисты должны запилить новую функциональность – создать фичу
  2. Прежде чем программисты начнут писать код (как это делается в большинстве случаев), и тестеры и программисты садятся за круглый стол и обсуждают – как именно фича будет работать. Результатом круглого стола является записанная на бумаге фича – набор действий клиента/пользователя, который приводит к некому результату: а) нажал сюда; б) ввел цифры туда; в) получил результат там


    В результате такого круглого стола создается одно понимание на всех данной фичи, задокументированное на бумаге


  3. Далее программисты начинают писать код, согласно описанной фичи. Тестеры также начинают параллельно писать тесты, ибо записанная фича, благодаря Cucumber, является будущим тестом. Понятно, что тест может быть закончен только после того, как закончат кодить программисты, но таким образом написание кода и тестов идет параллельно, что ускоряет процесс разработки

Еще плюсы Cucumber:


  • ненадобность логирования при написании тестов – каждый степ (действие пользователя) по сути своей является логированием.
  • человеко-понятное описание тестов – тесты будут понятны даже людям из бизнеса, что может пригодиться при демонстрировании отчетов о тестировании.
  • при описании бага не нужно придумывать steps to reproduce – необходимые степы берутся из отчета копипастом

Проект на гитхабе


Видео исполнения теста на youtube




Разберем первую, простую часть simple_selenide_cucumber.


Структура проекта:




Используем Intellij IDEA, Maven и Junit.


В mail.txt записаны логины, пароли аккаунтов для работы с тестом. ВНИМАНИЕ: если будете запускать у себя, имейте ввиду, что система выкинет одного из юзеров, которые будут логиниться под одним логином/паролем. Поменяйте мейл


В pom.xml прописываем следующие dependency:


<dependencies>

    <dependency>
        <groupId>com.codeborne</groupId>
        <artifactId>selenide</artifactId>
        <version>3.5</version>
    </dependency>

    <dependency>
        <groupId>info.cukes</groupId>
        <artifactId>cucumber-java8</artifactId>
        <version>1.2.3</version>
    </dependency>

    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>

</dependencies>

Файл smoketest#1.feature является той самой фичей (описанием фичи), которую согласовали программисты и тестеры за круглым столом (в идеальном мире:). Как видим, это описание действий пользователя на сайте, записанные в человеко-понятной форме, т.е. это еще и ваш файл логирования при условии, что каждый степ(действие) не подразумевает очень сложной логики:


Feature: smoke test #1, go through the service to Yandex-pay-page

  Scenario: go through the service to button "Купить"

    #actions at first page
    Given open riskmarket.ru
    When press button with text "Вход в кабинет"
    And type to input with name "userName" text: "riskmarket.testoviy2016@yandex.ru"
    And type to input with name "password" text: "l0dcfJMB"
    And press element with value "Войти"
...
  Scenario: go through service to yandex pay-page

    Given press button with text "КУПИТЬ"
    #actions at third page
    When type to input with name "lastName" text: "TESTOVIY"
...

Создание вашего UI теста начинается именно с этого файла, файл с расширением .feature. Вы должны поместить его в пакет test/java/…/features/


Фича должна начинаться с ключевого слова:


    Feature:

Здесь указывается общими словами что именно делает фича. В нашем случае smoke-теста это «Пройти через сервис до страницы Яндекс.платежей»


Далее идет ключевое слово:


    Scenario:

Сценарий фактически является отдельным тестом, т.е. фича может содержать сколько угодно сценариев (тестов). Все сценарии, очевидно, должны относиться к данной фиче. В нашем случае будет два сценария, первый – пройти до кнопки «Купить» и второй – пройти до страницы платежей. По правилам тестирования, сценарии (тесты) должны быть независимыми, т.е. успех прохождения одного сценария не должен зависеть от успеха прохождения второго сценария. ВНИМАНИЕ: в нашем случае это не выполняется – второй сценарий начинается на месте остановки первого сценария, и если первый свалится, то второй тоже.
У сценария также есть краткое описание – что именно он делает.


Далее идут сами степы. Перед каждым степом должно быть одно из ключевых слов Given, When, Then, And или But.


Given — обозначает начальные условия, «Дано: то-то и то-то»


When – действия пользователя: нажать сюда, подождать то


Then – результат, который получается: чаще всего это некая проверка, как в нашем случае


Then element with tag "search-result-item" should exist

и


Then verify that page with url "http://money.yandex.ru/cashdesk" is opened

And, But – используется как союз «и», «но», чтобы легче читалось. С «и» все ясно. «но» может использоваться, например, в степах, описывающих мысль «… эта штука должна быть видна, НО вот эта должна быть скрыта»


Старайтесь соблюдать разделение степов на указанные три части (Given, When, Then), т.к. это правила BDD/TDD.


После написания фичи вы можете запустить тест (правой клавишей по файлу фичи -> Run). Результатом будет много Undefined step: <текст степа>. Система намекает, что она не знает как выполнить каждый степ. Нужно подсунуть логику исполнения степа. Если вы пишете в ИДЕЕ, то у вас каждый неопределенный степ подсвечен. Нажмите Alt+Enter и пройдите по всем диалоговым окнам, не меняя значений. Будет создан класс MyStepdefs (я для удобства поместил его в пакет steps). Вы увидите что-то типа:


@Given("^open riskmarket\\.ru$")
public void openRiskmarketRu() throws Throwable
{
    // Write code here that turns the phrase above into concrete actions
    throw new PendingException();
}

По умолчанию методы, определяющие степы кидают PendingException(). Это нужно, чтобы не было неопределенных степов, и чтобы при этом можно было продолжать писать тесты. Т.е. пока фича пишется программистами, некоторые степы уже можно определить, а некоторые должны дождаться написания кода программистами. Каждый раз при запуске теста система будет напоминать вам какие именно степы еще не определены.


Вы также можете использовать лямбда-выражения для описания степов. Но я не буду разбирать это здесь, т.к. это отдельная тема. Будем делать по старинке.


Разберем определение степа подробнее:


@Given("^open riskmarket\\.ru$")
public void openRiskmarketRu()

Первая строчка – это аннотация, с помощью которой Cucumber понимает к какому именно степу относится данное определение. На месте @Given, как говорилось ранее, может стоять @And/@Then/@But/@When. Далее в аргументе аннотации используется регулярное выражение(regex).


Regex – это тема отдельной статьи, почитайте где-нибудь, материала полно.


Приведу ключевые используемые символы regex, которые нужны для старта:


  • ^ — начало строки
  • $ — конец строки
  • (.*) – какой угодно текст
  • "([^"]*)" – какой угодно текст, но в кавычках

Следующая строка public void openRiskmarketRu() это название метода. Метод, определяющий степ, всегда должен быть public void. Если вы используете Alt+Enter, то ИДЕЯ сама синтезирует название метода, чаще всего этого достаточно.


Разберем некоторые степы.


В описании логики степов используется Selenide


  1. Вид в фиче: Given open riskmarket.ru


    Вид в MyStepdefs:


    @Given("^open riskmarket\\.ru$")
    public void openRiskmarketRu()
    {
        open("http://riskmarket.ru");
    }

    Благодаря методу open(…) от Selenide в одной строчке создается instance WebDriver (по умолчанию Firefox) и происходит переход на указанный url. Закрывать/убивать instance не нужно, это сделает Selenide


  2. Вид в фиче:


    When press button with text "Вход в кабинет"`
    And press button with text "Рассчитать полис"
    Given press button with text "КУПИТЬ"
    And press button with text "Оплатить"

    Вид в MyStepdefs:


    @When("^press button with text \"([^\"]*)\"$")
    public void press(String button) 
    {
            $(byText(button)).waitUntil(Condition.visible, 15000).click();
    }

    Перед вами пример переиспользования степа. Старайтесь переиспользовать степы как можно чаще, не плодите код. В нашем примере в аргументе аннотации указываем, что «кнопка может содержать какой угодно текст, но в кавычках». Что прикольно, можно использовать любой язык.


    Вообще говоря, для описания степов также можно использовать любой язык – можно писать так:


     And нажать кнопку с текстом "Оплатить"

    Раз название кнопки – это аргумент, то указываем его в сигнатуре метода:


        public void press(String button)

    $() – это метод Selenide для поиска элемента на странице. У него есть много разных, удобных параметров. В данной случае ищем элемент, который содержит наш текст. Пишу статью из места с не очень быстрым интернетом, поэтому нужно добавить увеличенное ожидание, пока элемент не появится, т.к. встроенного таймаута на 4с не хватает. $(byText(button) дает нам объект типа SelenideElement, у которого среди прочих методов есть такое ожидание – waitUntil(Condition, timeout). Condition – условие, которое мы ждем.


    Condition – это класс Selenide, в котором описаны много разных условий, посмотрите, пригодится.


    И в конце, когда мы дождались появления элемента, кликаем по нему.
    К слову, то, что здесь описано в одну строку, в чистом Selenium у вас бы заняло несколько строчек кода, с созданием WebDriverWait.


  3. Вид в фиче:


    And type to input with name "userName" text: "riskmarket.testoviy2016@yandex.ru"
    And type to input with name "password" text: "l0dcfJMB”
    When type to input with name "lastName" text: "TESTOVIY"
    And type to input with name "firstName" text: "TEST"

    Вид в MyStepdefs:


    @And("^type to input with name \"([^\"]*)\" text: \"([^\"]*)\"$")
    public void typeToInputWithNameText(String input, String text)
    {   
        sleep(1000);
        $(byName(input)).sendKeys(text);
    }

    Данный степ используется в один из разов после появления фрейма, поэтому нужно сделать паузу, чтобы input успел появится – делается с помощью Selenide sleep(timeout with ms).


    sendKeys(String) — отрпавляет текст в элемент.


  4. Вид в фиче:


    And select countries: Шенген, Финляндия, Китай

    Вид в MyStepdefs:


    @And("^select countries: (.*)$")
    public void selectCountries(List<String> countries)
    {
        for (String str : countries)
        {
            $("#countryInput").sendKeys(str);
            $("#countryInput").pressEnter();
        }
    }

    При описании степов как параметр можно принимать списки – элементы перечисляются через запятую.
    Остальные степы похожи на описанные выше.
    В pom.xml в этот проект был добавлен Junit только из-за последнего степа, где проверка, что открылся нужный url, происходит с помощью assertThat().



На этом первая часть заканчивается. Читайте во второй части про автоматические скриншоты, кастомные Condition, PageObject, аннотацию элементов и создание красивых отчетов.

Поделиться с друзьями
-->

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


  1. monolithed
    12.05.2016 13:11
    +1

    Вдруг кому пригодится, для JavaScript есть похожее решение с WebdriverIO + Adapter for Cucumber


  1. m00t
    12.05.2016 20:49
    +1

    And type to input with name "userName" text: "riskmarket.testoviy2016@yandex.ru"
    And type to input with name "password" text: "l0dcfJMB”
    When type to input with name "lastName" text: "TESTOVIY"
    And type to input with name "firstName" text: "TEST"


    То, что вы делаете, считается антипаттерном в BDD. Сценарии должны быть на языке домена:
    Given there is user Alice


  1. m00t
    12.05.2016 20:53

    В целом BDD это не совсем про тесты — это про документацию и коммуникацию, скорее. Из всех фреймворков поэтому давно поудаляли встроенные степы «я нажимаю на кнопку».


    1. vasidzius
      13.05.2016 10:49

      Хороший наброс. Приложите, пожалуйста, пруф, в частности, названия компаний и/или фреймворков, в которых «давно поудаляли встроенные степы «я нажимаю на кнопку»». Спасибо.


      1. m00t
        13.05.2016 11:17

        Прошу прощения, обманул насчет всех. Нашел только в оригинальном cucumber:


      1. m00t
        13.05.2016 11:37

        А, вот, нашел планы по удалению их в php-кукумбере тоже: https://twitter.com/BehatPHP/status/700679895316373504?lang=en


      1. m00t
        13.05.2016 11:39

        Так что это только вопрос времени, когда до всех [your language]-кукумберов дойдет, что эти степы не нужны в BDD-фреймворке.


        1. vasidzius
          13.05.2016 12:53

          В целом согласен. Но считаю, что статья по-прежнему актуальна для джунов (в том числе меня), ибо писалась именно с целью разжевать как начать писать тесты на кукумбере с селенидом. Содержимое статьи никак не мешает использовать кукумбер правильным способом.


  1. Andrey79
    13.05.2016 19:25

    Скажите, а какой коммандой запустить эти тесты из консоли?


    1. vasidzius
      16.05.2016 12:23

      Хороший вопрос. В первой части, в simple варианте, запускать можно только фичу (smoketest#1.feature). И как запустить ее из командной строки я не знаю. Во второй части, в complex варианте, когда присутствует класс-раннер, сработает команда mvn test (или mvn clean test), потому что IDEA понимает, что тест — это класс-раннер, SmokeTest.java. Вообще говоря, simple вариант не несет себе практической ценности, только как обучение. В реальности всегда нужен будет класс-раннер, потому что без него не получится сформировать отчет о тестах. Так что посмотрите как создается класс-раннер и запускайте указанной командой — mvn clean test. Ну и если дойдете до создания beautiful reports, то используйте команду mvn clean install


  1. Andrey79
    16.05.2016 14:51

    Я запустил complex вариант коммандой mvn test и получил ошибку, подскажите, пожалуйста, как это исправить
    Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.417 sec <<< FAILURE!
    initializationError(ru.riskmarket.runners.SmokeTest) Time elapsed: 0.01 sec <<< ERROR!
    java.lang.UnsupportedClassVersionError: cucumber/runtime/java8/ConstantPoolTypeIntrospector: Unsupported major.minor version 52.0
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:791)
    at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
    at java.net.URLClassLoader.defineClass(URLClassLoader.java:449)
    at java.net.URLClassLoader.access$100(URLClassLoader.java:71)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:361)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:355)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(URLClassLoader.java:354)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:423)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:356)
    at cucumber.runtime.io.ResourceLoaderClassFinder.loadClass(ResourceLoaderClassFinder.java:38)
    at cucumber.runtime.io.ResourceLoaderClassFinder.getDescendants(ResourceLoaderClassFinder.java:26)
    at cucumber.runtime.Reflections.instantiateSubclasses(Reflections.java:28)
    at cucumber.runtime.Runtime.loadBackends(Runtime.java:98)
    at cucumber.runtime.Runtime.(Runtime.java:65)
    at cucumber.api.junit.Cucumber.createRuntime(Cucumber.java:78)
    at cucumber.api.junit.Cucumber.(Cucumber.java:58)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:57)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:525)
    at org.junit.internal.builders.AnnotatedBuilder.buildRunner(AnnotatedBuilder.java:104)
    at org.junit.internal.builders.AnnotatedBuilder.runnerForClass(AnnotatedBuilder.java:86)
    at org.junit.runners.model.RunnerBuilder.safeRunnerForClass(RunnerBuilder.java:59)
    at org.junit.internal.builders.AllDefaultPossibilitiesBuilder.runnerForClass(AllDefaultPossibilitiesBuilder.java:26)
    at org.junit.runners.model.RunnerBuilder.safeRunnerForClass(RunnerBuilder.java:59)
    at org.junit.internal.requests.ClassRequest.getRunner(ClassRequest.java:33)
    at org.apache.maven.surefire.junit4.JUnit4Provider.execute(JUnit4Provider.java:250)
    at org.apache.maven.surefire.junit4.JUnit4Provider.executeTestSet(JUnit4Provider.java:141)
    at org.apache.maven.surefire.junit4.JUnit4Provider.invoke(JUnit4Provider.java:112)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:601)
    at org.apache.maven.surefire.util.ReflectionUtils.invokeMethodWithArray(ReflectionUtils.java:189)
    at org.apache.maven.surefire.booter.ProviderFactory$ProviderProxy.invoke(ProviderFactory.java:165)
    at org.apache.maven.surefire.booter.ProviderFactory.invokeProvider(ProviderFactory.java:85)
    at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:115)
    at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:75)


    1. asolntsev
      16.05.2016 16:12

      @ Andrey79 Вы запускаете на Java 7, а этот Cucumber требует Java 8.
      Обновите у себя Java.


  1. Andrey79
    16.05.2016 16:24

    У меня Java 8
    java version «1.8.0_92»
    Java(TM) SE Runtime Environment (build 1.8.0_92-b14)
    Java HotSpot(TM) 64-Bit Server VM (build 25.92-b14, mixed mode


    1. asolntsev
      16.05.2016 16:32

      Вероятно, maven использует не эту версию Java.
      Он находит где-то Java 7 (или даже 6) и запускает её. Попробуйте запустить «mvn -v», она всё покажет.


      1. Andrey79
        16.05.2016 16:54

        Спасибо большое, я исправил JAVA_HOME на корректную версию джавы и тест запустился.