Зачем нам вулканец на борту: обзор Spock Framework

    Автоматизация тестирования помогает постоянно контролировать качество IT-продукта, а также снижать затраты в долгосрочной перспективе. В автоматизации существуют различные подходы, например, Behavior Driven Development (BDD), разработка через поведение.

    С этим подходом связаны инструменты cucumber, robot framework, behave и другие, в которых разделены сценарии выполнения и реализация каждой конструкции. Такое разделение помогает составить удобочитаемые сценарии, но требует значительных затрат времени и поэтому может быть непрактичным при написании реализации.

    Рассмотрим, как можно упростить работу с BDD, используя подходящие инструменты – например, фреймворк Spock, который сочетает в себе красоту, удобство принципов BDD и особенности jUnit.



    Spock framework


    Spock – фреймворк для тестирования и спецификации приложений на языках Java и Groovy. Благодаря использованию в качестве основы платформы JUnit этот фреймворк совместим со всеми популярными IDE (в частности, IntelliJ IDEA), различными инструментами сборки (Ant, Gradle, Maven) и continuous integration (CI) серверами.

    Как пишут разработчики фреймворка, Spock «вдохновлен JUnit, RSpec, jMock, Mockito, Groovy, Scala, вулканцами и другими увлекательными формами жизни».

    В этой статье мы рассмотрим последнюю доступную версию, Spock Framework 2.0. Ее особенности: возможность использования JUnit5, Java 8+, groovy 2.5 (также существует сборка с версией 3.0). Spock распространяется по лицензии Apache 2.0 и имеет отзывчивое сообщество пользователей. Разработчики фреймворка продолжают дорабатывать и развивать Spock, который уже включает в себя множество расширений, позволяющих тщательно настроить запуск тестов. Например, одно из наиболее интересных анонсированных направлений доработки – это добавление параллельного исполнения тестов.

    Groovy


    Groovy является объектно-ориентированным языком программирования, разработанным для платформы Java как дополнение с возможностями Python, Ruby и Smalltalk. Groovy использует Java-подобный синтаксис с динамической компиляцией в JVM байт-код и напрямую работает с другим Java-кодом и библиотеками. Язык может использоваться в любом Java-проекте или как скриптовый язык.

    К особенностям groovy относятся: как статическая, так и динамическая типизация; встроенный синтаксис для списков, массивов и регулярных выражений; перегрузка операций. При этом замыкания в Groovy появились задолго до Java.

    Groovy хорошо подходит для быстрой разработки тестов, когда есть возможность применять «синтаксический сахар», схожий с python, и не задумываться о типизации объектов.

    Особенности Spock Framework


    Одна из ключевых особенностей фреймворка – у разработчика есть возможность писать спецификации с ожидаемыми характеристиками системы с использованием принципов BDD подхода. Этот подход позволяет составлять бизнес-ориентированные функциональные тесты для программных продуктов с высокой предметной и организационной сложностью.

    Спецификация представляет собой класс groovy, расширяющий spock.lang.Specification

    class MyFirstSpecification extends Specification {
      // fields
      // fixture methods
      // feature methods
      // helper methods
    }

    Спецификация может содержать различные вспомогательные поля, которые инициируются для каждого класса спецификации.

    С помощью аннотации @Shared можно дать доступ к полю классам-наследникам спецификации.

    abstract class PagesBaseSpec extends Specification {
    
        @Shared
        protected WebDriver driver
    
    
        def setup() {
            this.driver = DriverFactory.createDriver()
            driver.get("www.anywebservice.ru")
        }
    
        void cleanup() {
            driver.quit()
        }
    
    }

    Методы настройки класса спецификации:

    def setupSpec() {} // запускается при работе первого feature метода из спецификации 
    def setup() {}     // запускается перед каждым feature методом
    def cleanup() {}   // запускается после каждого feature метода
    def cleanupSpec() {} // запускается после работы последнего feature метода из спецификации

    В следующей таблице рассмотрим, у каких ключевых слов и методов Spock framework есть аналоги в JUnit.



    Блоки теста


    В Spock Framework каждая фаза теста выделена в отдельный блок кода (см. пример в документации).



    Блок кода начинается с лейбла и завершается началом следующего блока кода или окончанием теста.

    Блок given отвечает за настройку начальных условий теста.

    Блоки when, then всегда используются вместе. В блоке when – стимулятор, раздражитель системы, а в блоке then – ответная реакция системы.

    В тех случаях, когда есть возможность сократить конструкцию when-then до одного выражения, можно использовать один блок expect. Далее будут использованы примеры из официальной документации Spock framework:

    when:
    def x = Math.max(1, 2)
     
    then:
    x == 2
    

    или одно выражение

    expect:
    Math.max(1, 2) == 2

    Блок cleanup применяют для освобождения ресурсов перед следующей итерацией теста.

    given:
    def file = new File("/some/path")
    file.createNewFile()
     
    // ...
     
    cleanup:
    file.delete()
    

    Блок where применяют для передачи данных для тестирования (Data Driven Testing).

    def "computing the maximum of two numbers"() {
      expect:
      Math.max(a, b) == c
     
      where:
      a << [5, 3]
      b << [1, 9]
      c << [5, 9]
    }

    Виды передачи входных данных будут рассмотрены далее.

    Пример реализации теста на Spock Framework


    Далее рассмотрим подходы к реализации тестирования веб-страницы авторизации пользователя в системе с использованием selenium.

    import helpers.DriverFactory
    import org.openqa.selenium.WebDriver
    import spock.lang.Shared
    import spock.lang.Specification
    
    abstract class PagesBaseSpec extends Specification {
    
        @Shared
        protected WebDriver driver
        
        def setup() {
            this.driver = DriverFactory.createDriver()
            driver.get("www.anywebservice.ru")
        }
    
        void cleanup() {
            driver.quit()
        }
    }
    

    Здесь мы видим базовый класс спецификации страницы. В начале класса мы видим импорт необходимых классов. Далее представлена аннотация shared, позволяющая классам-наследникам получить доступ к веб-драйверу. В блоке setup() мы видим код инициализации веб-драйвера и открытия веб-страницы. В блоке cleanup() – код завершения работы веб-драйвера.

    Далее перейдем к обзору спецификации страницы авторизации пользователя.

    import pages.LoginPage
    import spock.lang.Issue
    
    class LoginPageTest extends PagesBaseSpec {
    
        @Issue("QAA-1")
        def "QAA-1: Authorization with correct login and password"() {
    
            given: "Login page"
            def loginPage = new LoginPage(driver)
    
            and: "Correct login and password"
            def adminLogin = "adminLogin"
            def adminPassword = "adminPassword"
    
            when: "Log in with correct login and password"
            loginPage.login(adminLogin, adminPassword)
    
            then: "Authorized and moved to main page"
            driver.currentUrl == "www.anywebservice.ru/main"
        }
    }
    

    Спецификация страницы авторизации наследуется от базовой спецификации страниц. Аннотация Issue задает идентификатор теста во внешней системе трекинга (например, Jira). В следующей строке мы видим название теста, которое по соглашению задается строковыми литералами, что позволяет использовать любые символы в названии теста (в том числе и русскоязычные). В блоке given происходит инициализация page object класса страницы авторизации, а также получение корректных логина и пароля для авторизации в системе. В блоке when выполняется действие по авторизации. В блоке then – проверка ожидаемого действия, а именно – успешная авторизация и переадресация на главную страницу системы.

    На примере данной спецификации мы видим наиболее значимый плюс использования парадигмы BDD в spock – спецификация системы одновременно является и ее документацией. Каждый тест описывает определенное поведение, каждый шаг в тесте имеет свое описание, понятное не только разработчикам, но и заказчикам. Описание блоков может быть представлено не только в исходном коде теста, но и в диагностических сообщениях или отчетах о работе теста.

    В фреймворке предусмотрена возможность передавать различные логины и пароли для тестирования (параметризировать тест).

    Data Driven Testing в Spock Framework


    Data Driven Testing = table-driven testing = parameterized testing

    Для тестирования сценария с несколькими параметрами можно использовать различные варианты их передачи.

    Таблицы данных (Data Tables)


    Рассмотрим несколько примеров из официальной документации фреймворка.

    class MathSpec extends Specification {
      def "maximum of two numbers"() {
        expect:
        Math.max(a, b) == c
     
        where:
        a | b | c
        1 | 3 | 3
        7 | 4 | 7
        0 | 0 | 0
      }
    }
    

    Каждая строка в таблице – отдельная итерация теста. Также таблица может быть представлена и одним столбцом.

    where:
    a | _
    1 | _
    7 | _
    0 | _

    _ — объект-заглушка класса спецификации.

    Для лучшего визуального восприятия параметров можно переписать пример выше в следующем виде:

    def "maximum of two numbers"() {
        expect:
        Math.max(a, b) == c
     
        where:
        a | b || c
        1 | 3 || 3
        7 | 4 || 7
        0 | 0 || 0
    }
    

    Теперь мы видим, что a, b – входные параметры, а c – ожидаемое значение.

    Потоки данных (Data pipes)


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

    ...
    where:
    a << [1, 7, 0]
    b << [3, 4, 0]
    c << [3, 7, 0]

    Здесь левый сдвиг << – перегруженный groovy оператор, который теперь выполняет роль добавления элементов в список.

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

    1 итерация: a=1, b=3, c=3;
    2 итерация: a=7, b=4, c=7;
    3 итерация: a=0, b=0, c=0.

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

    @Shared sql = Sql.newInstance("jdbc:h2:mem:", "org.h2.Driver")
     
    def "maximum of two numbers"() {
      expect:
      Math.max(a, b) == c
     
      where:
      [a, b, c] << sql.rows("select a, b, c from maxdata")
    }

    Переменная как данные (Data Variable Assignment)


    ...
    where:
    a = 3
    b = Math.random() * 100
    c = a > b ? a : b

    Здесь мы видим динамически вычисляемую переменную c в тестовых данных.

    Комбинация различных видов передачи параметров


    ...
    where:
    a | _
    3 | _
    7 | _
    0 | _
     
    b << [5, 0, 0]
     
    c = a > b ? a : b

    Никто не запрещает вам применять сразу несколько видов передачи, если это необходимо.

    Пример реализации параметризованного теста на Spock Framework


    @Issue("QAA-1-parametrized")
    def "QAA-1-parametrized: Authorization with correct login and password"() {
    
       given: "Login page"
       def loginPage = new LoginPage(driver)
    
       when: "Log in with correct login and password"
       loginPage.login(login, password)
    
       then: "Authorized and moved to main page"
       driver.currentUrl =="www.anywebservice.ru/main"
    
       where: "Check for different logins and passwords"
       login            | password
       "adminLogin"     | "adminPassword"
       "moderatorLogin" | "moderatorPassword"
       "userLogin"      | "userPassword"
    }

    Здесь мы видим уже знакомый нам блок where, в котором заданы ключи параметров (логинов и паролей), которые хранятся в файле конфигурации.

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

    Пример реализации параметризованного теста с доработанной спецификацией


    До доработки


    abstract class PagesBaseSpec extends Specification {
    
        @Shared
        protected WebDriver driver
    
    
        def setup() {
            this.driver = DriverFactory.createDriver()
            driver.get("www.anywebservice.ru")
        }
    
        void cleanup() {
            driver.quit()
        }
    
    }

    После доработки


    import helpers.DriverFactory
    import org.openqa.selenium.WebDriver
    import spock.lang.Shared
    import spock.lang.Specification
    
    abstract class PagesNoRestartBaseSpec extends Specification {
    
        @Shared
        protected WebDriver driver
    
        def setupSpec() {
            this.driver = DriverFactory.createDriver()
        }
    
        def setup() {
            this.driver.get("www.anywebservice.ru")
        }
    
        def cleanup() {
            this.driver.get("www.anywebservice.ru/logout")
            this.driver.manage().deleteAllCookies();
        }
    
        void cleanupSpec() {
            this.driver.quit()
        }
    }

    В обновленной спецификации мы видим, что процедура создания веб-драйвера будет выполняться только при настройке класса спецификации, а закрытие браузера – только после завершения работы тестов из спецификации. В методе setup() мы видим тот же код получения веб-адреса сервиса и его открытие в браузере, а в методе cleanup() – переход по адресу www.anywebservice.ru/logout для завершения работы с сервисом у текущего пользователя и удаления файлов куки (для тестирования текущего веб-сервиса данной процедуры достаточно, чтобы имитировать «уникальный» запуск). Код самого теста не изменился.

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

    Сравнение тестов на testNG, pytest, pytest-bdd


    Для начала мы рассмотрим реализацию теста на тестовом фреймворке testNG на языке программирования Java, который также, как и Spock Framework, вдохновлен фреймворком jUnit и поддерживает data-driven testing.

    package javaTests;
    
    import org.testng.Assert;
    import org.testng.annotations.*;
    import pages.LoginPage;
    
    
    public class LoginPageTest extends BaseTest {
    
    
        @BeforeClass
        public final void setup() {
            createDriver();
            driver.get("www.anywebservice.ru");
        }
    
        @DataProvider(name = "userParameters")
        public final Object[][] getUserData(){
            return new Object[][] {
                    {"adminLogin", "adminPassword"},
                    {"moderatorLogin", "moderatorPassword"},
                    {"userLogin", "userPassword"}
            };
        }
    
        @Test(description = "QAA-1-1: Authorization with correct login and password",
                dataProvider = "userParameters")
        public final void authorizationWithCorrectLoginAndPassword(String login, String password){
            //Login page
            LoginPage loginPage = new LoginPage(driver);
    
            //Log in with correct login and password
            loginPage.login(login, password);
    
            //Authorized and moved to main page
            Assert.assertEquals("www.anywebservice.ru/main", driver.getCurrentUrl());
        }
    
        @AfterMethod
        public final void cleanup() {
            driver.get("www.anywebservice.ru/logout");
            driver.manage().deleteAllCookies();
        }
    
        @AfterClass
        public final void tearDown() {
            driver.quit();
        }
    }

    Здесь мы можем видеть тестовый класс со всеми необходимыми setup(), cleanup() методами, а также параметризацию теста в виде дополнительного метода getUserData() с аннотацией @DataProvider, что выглядит несколько громоздко, после того, что мы рассмотрели в тесте с использованием Spock Framework. Также для понимания того, что происходит в тесте, были оставлены комментарии, аналогичные описанию шагов.

    Стоит отметить, что в testNG, в отличие от Spock Framework, реализована поддержка параллельного выполнения теста.



    Далее перейдем к тесту с использованием тестового фреймворка pytest на языке программирования Python.

    import pytest
    from selenium.webdriver.support import expected_conditions
    from selenium.webdriver.support.wait import WebDriverWait
    
    from PageObjects.LoginPage import LoginPage
    
    
    class TestLogin(object):
    
        @pytest.mark.parametrize("login,password", [
            pytest.param(("adminLogin", "adminPassword"), id='admin'),
            pytest.param(("moderatorLogin", "moderatorPassword"), id='moderator'),
            pytest.param(("userLogin", "userPassword"), id='user')
        ])
        def test_authorization_with_correct_login_and_password(self, login, password, driver, test_cleanup):
            # Login page
            login_page = LoginPage(driver)
            # Log in with correct login and password
            login_page.login(login, password)
    
            # Authorized and moved to main page
            assert expected_conditions.url_to_be("www.anywebservice.ru/main")
     
        @pytest.fixture()
        def test_cleanup(self, driver):
            yield "test"
            driver.get("www.anywebservice.ru/logout")
            driver.delete_all_cookies()
    

    Здесь мы также видим поддержку data-driven testing в виде отдельной конструкции, схожей с @DataProvider в testNG. Метод настройки веб-драйвера «спрятан» в фикстуре driver. Благодаря динамической типизации и фикстурам pytest, код этого теста выглядит чище, чем на Java.



    Далее перейдем к обзору кода теста с использованием плагина pytest-bdd, который позволяет писать тесты в виде feature файлов Gherkin (чистый BDD-подход).

    login.feature

    Feature: Login page
      A authorization
    
      Scenario: Authorizations with different users
        Given Login page
        When Log in with correct login and password
        Then Authorized and moved to main page
    

    test_login.py

    import pytest
    from pytest_bdd import scenario, given, when, then
    from selenium.webdriver.support import expected_conditions
    from selenium.webdriver.support.wait import WebDriverWait
    
    from PageObjects.LoginPage import LoginPage
    
    
    @pytest.mark.parametrize("login,password", [
        pytest.param(("adminLogin", "adminPassword"), id='admin'),
        pytest.param(("moderatorLogin", "moderatorPassword"), id='moderator'),
        pytest.param(("userLogin", "userPassword"), id='user')
    ])
    @scenario('login.feature', 'Authorizations with different users')
    def test_login(login, password):
        pass
    
    
    @given('Login page')
    def login_page(driver):
        return LoginPage(driver)
    
    
    @when('Log in with correct login and password')
    def login_with_correct_login_and_password(login_page, login, password):
        login_page_object = login_page
        login_page_object.login(login, password)
    
    @then('Authorized and moved to main page')
    def authorized_and_moved_to_main_page(driver, login):
        assert expected_conditions.url_to_be("www.anywebservice.ru/main")

    Из плюсов можно выделить то, что это все еще фреймворк pytest, который имеет множество плагинов для различных ситуаций, в том числе и для параллельного запуска тестов. Из минусов – сам чистый BDD-подход, который будет постоянно ограничивать разработчика своими особенностями. Spock Framework дает возможность писать более лаконичный и простой в оформлении код, по сравнению со связкой PyTest + pytest-bdd.



    Заключение


    В этой статье мы рассмотрели возможность упрощения работы с BDD с помощью фреймворка Spock. Подводя итоги, кратко выделим основные, на наш взгляд, плюсы и минусы Spock по сравнению с некоторыми другими распространенными тестовыми фреймворками.

    Плюсы:

    • Использование принципов BDD вместо чистого BDD-подхода дает большую гибкость при написании тестов.
    • Написанная тестовая спецификация является также и документацией системы.
    • Наличие различных расширений для настройки тестов.
    • Язык groovy (динамическая типизация, синтаксический сахар, closures или замыкания).

    Минусы:

    • Динамическая типизация языка groovy. Поскольку применяется динамическая типизация, то механизмы предугадывания, используемые в IDE для анализа содержимого переменной, при долгой работе могут начать сбоить. Если рассматривать Intellij IDEA, то постоянно ведутся доработки в этом направлении, что, несомненно, радует.
    • Динамическая компиляция groovy кода в JVM байт-код. Если кратко, то не стоит писать все подряд на groovy, поскольку вы можете существенно проиграть во времени компиляции данного кода, особенно если он занимает много строк кода и часто используется. Важные части своего тестового фреймворка все же стоит писать на java, а groovy оставить для тестов.
    • Набор расширений не такой обширный, как у testNG, к примеру. Как следствие – отсутствие параллельного запуска тестов. Есть планы добавить эту функциональность, но сроки их реализации неизвестны.

    В конечном итоге, инструмент Spock Framework, несомненно, заслуживает внимания, поскольку подходит для решения сложной распространенной задачи автоматизации бизнес-сценариев для программных продуктов с высокой предметной и организационной сложностью.

    Что еще можно почитать:

    SimbirSoft
    Лидер в разработке современных ИТ-решений на заказ

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

      0
      Набор расширений не такой обширный, как у testNG, к примеру. Как следствие – отсутствие параллельного запуска тестов. Есть планы добавить эту функциональность, но сроки их реализации неизвестны.

      Эту проблему легко исправить если использовать Gradle вместе со Spock. Сразу появляется возможность параллельного запуска тестов, а так же становятся доступными другие фишки Gradle
        0
        Добрый день! Мы стремились в первую очередь разобрать решения из коробки. Так как прямого указания на Gradle в Spock не было, то в статье его не упоминаем, но само по себе это интересное решение. Спасибо)

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое