Введение в тестирование со Споком и Groovy

Введение в тестирование со Споком и Groovy

1. Вступление

В этой статье мы рассмотримSpock, платформу тестированияGroovy. Главным образом, Spock стремится стать более мощной альтернативой традиционному стеку JUnit, используя возможности Groovy.

Groovy - это язык на основе JVM, который легко интегрируется с Java. Помимо функциональной совместимости, он предлагает дополнительные концепции языка, такие как динамический, имеющий дополнительные типы и метапрограммирование.

Используя Groovy, Спок представляет новые выразительные способы тестирования наших приложений Java, которые просто невозможны в обычном коде Java. В этой статье мы рассмотрим некоторые высокоуровневые концепции Спока с некоторыми практическими пошаговыми примерами.

2. Maven Dependency

Прежде чем мы начнем, давайте добавим нашMaven dependencies:


    org.spockframework
    spock-core
    1.0-groovy-2.4
    test


    org.codehaus.groovy
    groovy-all
    2.4.7
    test

Мы добавили и Spock, и Groovy, как любую стандартную библиотеку. Однако, поскольку Groovy - новый язык JVM, нам необходимо включить плагинgmavenplus, чтобы иметь возможность его скомпилировать и запустить:


    org.codehaus.gmavenplus
    gmavenplus-plugin
    1.5
    
        
            
                compile
                testCompile
            
        
     

Теперь мы готовы написать наш первый тест Спока, который будет написан на Groovy-коде. Обратите внимание, что мы используем Groovy и Spock только для целей тестирования, и именно поэтому эти зависимости имеют тестовую область.

3. Структура теста Спока

3.1. Технические характеристики и особенности

Поскольку мы пишем наши тесты на Groovy, нам нужно добавить их в каталогsrc/test/groovy вместоsrc/test/java.. Давайте создадим наш первый тест в этом каталоге, назвав егоSpecification.groovy:

class FirstSpecification extends Specification {

}

Обратите внимание, что мы расширяем интерфейсSpecification. Каждый класс Спока должен расширять это для того, чтобы сделать его доступным. Это позволяет нам реализовать наши первыеfeature:

def "one plus one should equal two"() {
  expect:
  1 + 1 == 2
}

Прежде чем объяснять код, стоит также отметить, что в Spock то, что мы называемfeature, в некоторой степени синонимично тому, что мы видим какtest в JUnit. Итак,whenever we refer to a feature we are actually referring to a test.

Теперь давайте проанализируем нашfeature. При этом мы сразу должны увидеть некоторые различия между ним и Java.

Первое отличие состоит в том, что имя метода объекта записывается в виде обычной строки. В JUnit у нас было бы имя метода, в котором используются верблюд или регистр символов для разделения слов, которые не были бы такими выразительными или удобочитаемыми для человека.

Следующее, что наш тестовый код находится в блокеexpect. Мы рассмотрим блоки более подробно в ближайшее время, но по сути они являются логическим способом разделения различных этапов наших тестов.

Наконец, мы понимаем, что нет никаких утверждений. Это потому, что утверждение является неявным: оно проходит, когда наше выражение равноtrue, и терпит неудачу, когда оно равноfalse. Опять же, вскоре мы рассмотрим утверждения более подробно.

3.2. Блоки

Иногда при написании теста JUnit мы можем заметить, что нет выразительного способа разбить его на части. Например, если бы мы следили за разработкой, управляемой поведением, мы могли бы в конечном итоге обозначать частиgiven when then с помощью комментариев:

@Test
public void givenTwoAndTwo_whenAdding_thenResultIsFour() {
   // Given
   int first = 2;
   int second = 4;

   // When
   int result = 2 + 2;

   // Then
   assertTrue(result == 4)
}

Спок решает эту проблему с блоками. Blocks are a Spock native way of breaking up the phases of our test using labels. Они дают нам метки дляgiven when then и более:

  1. Setup (псевдоним Given) - здесь мы выполняем любую настройку, необходимую перед запуском теста. Это неявный блок, в котором код, не входящий ни в один блок, становится его частью.

  2. When - здесь мы предоставляемstimulus тому, что проходит тестирование. Другими словами, где мы вызываем наш тестируемый метод

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

  4. Expect - это способ выполнения нашихstimulus иassertion в одном блоке. В зависимости от того, что мы находим более выразительным, мы можем или не можем использовать этот блок

  5. Cleanup - Здесь мы удаляем все ресурсы тестовых зависимостей, которые в противном случае остались бы позади. Например, мы можем захотеть удалить любые файлы из файловой системы или удалить тестовые данные, записанные в базу данных

Давайте снова попробуем реализовать наш тест, на этот раз полностью используя блоки:

def "two plus two should equal four"() {
    given:
        int left = 2
        int right = 2

    when:
        int result = left + right

    then:
        result == 4
}

Как мы видим, блоки помогают нашему тесту стать более читабельным.

3.3. Использование Groovy-функций для утверждений

Within the then and expect blocks, assertions are implicit.

Чаще всего выполняется оценка каждого оператора, а затем происходит сбой, если он не равенtrue. Связывая это с различными функциями Groovy, он удаляет ненужную библиотеку утверждений. Давайте попробуем утверждениеlist, чтобы продемонстрировать это:

def "Should be able to remove from list"() {
    given:
        def list = [1, 2, 3, 4]

    when:
        list.remove(0)

    then:
        list == [2, 3, 4]
}

Хотя в этой статье мы лишь вкратце коснемся Groovy, стоит объяснить, что здесь происходит.

Во-первых, Groovy дает нам более простые способы создания списков. Мы можем просто объявить наши элементы в квадратных скобках, и внутренне будет создан экземплярlist.

Во-вторых, поскольку Groovy является динамическим, мы можем использоватьdef, что просто означает, что мы не объявляем тип для наших переменных.

Наконец, в контексте упрощения нашего теста наиболее полезной демонстрируемой функцией является перегрузка операторов. Это означает, что внутренне, вместо того, чтобы выполнять сравнение ссылок, как в Java, методequals() будет вызываться для сравнения двух списков.

Также стоит продемонстрировать, что происходит, когда наш тест не проходит. Давайте разберемся, а затем посмотрим, что выводится на консоль:

Condition not satisfied:

list == [1, 3, 4]
|    |
|    false
[2, 3, 4]
 

at FirstSpecification.Should be able to remove from list(FirstSpecification.groovy:30)

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

3.4. Утверждение исключений

Спок также предоставляет нам выразительный способ проверки исключений. В JUnit некоторые наши варианты могут использовать блокtry-catch, объявитьexpected в верхней части нашего теста или использовать стороннюю библиотеку. В собственных утверждениях Спока есть готовый способ борьбы с исключениями:

def "Should get an index out of bounds when removing a non-existent item"() {
    given:
        def list = [1, 2, 3, 4]

    when:
        list.remove(20)

    then:
        thrown(IndexOutOfBoundsException)
        list.size() == 4
}

Здесь нам не пришлось вводить дополнительную библиотеку. Еще одно преимущество состоит в том, что методthrown() подтвердит тип исключения, но не остановит выполнение теста.

4. Тестирование на основе данных

4.1. Что такое тестирование на основе данных?

По сути,data driven testing is when we test the same behavior multiple times with different parameters and assertions. Классическим примером этого будет тестирование математической операции, такой как возведение в квадрат числа. В зависимости от различных комбинаций операндов результат будет разным. В Java термин, с которым мы можем быть более знакомы, является параметризованным тестированием.

4.2. Реализация параметризованного теста на Java

Для некоторого контекста стоит реализовать параметризованный тест с использованием JUnit:

@RunWith(Parameterized.class)
public class FibonacciTest {
    @Parameters
    public static Collection data() {
        return Arrays.asList(new Object[][] {
          { 1, 1 }, { 2, 4 }, { 3, 9 }
        });
    }

    private int input;

    private int expected;

    public FibonacciTest (int input, int expected) {
        this.input = input;
        this.expected = expected;
    }

    @Test
    public void test() {
        assertEquals(fExpected, Math.pow(3, 2));
    }
}

Как мы видим, здесь довольно много многословия, и код не очень удобочитаем. Нам пришлось создать двумерный массив объектов, который живет вне теста, и даже объект-оболочку для внедрения различных тестовых значений.

4.3. Использование таблиц данных в Spock

Спок, по сравнению с JUnit, легко выигрывает благодаря тому, что он чисто реализует параметризованные тесты. Опять же, в Spock это известно какData Driven Testing.. Теперь давайте снова реализуем тот же тест, только на этот раз мы будем использовать Spock сData Tables, что обеспечивает гораздо более удобный способ выполнения параметризованного теста. :

def "numbers to the power of two"(int a, int b, int c) {
  expect:
      Math.pow(a, b) == c

  where:
      a | b | c
      1 | 2 | 1
      2 | 2 | 4
      3 | 2 | 9
  }

Как мы видим, у нас есть простая и выразительная таблица данных, содержащая все наши параметры.

Кроме того, он относится к тому, что он должен делать, наряду с тестом, и нет шаблона. Тест выразительный, с понятным для человека именем и чистым блокомexpect иwhere для разбиения логических разделов.

4.4. Когда Datatable терпит неудачу

Также стоит посмотреть, что происходит, когда наш тест не проходит:

Condition not satisfied:

Math.pow(a, b) == c
     |   |  |  |  |
     4.0 2  2  |  1
               false

Expected :1

Actual   :4.0

Опять же, Спок дает нам очень информативное сообщение об ошибке. Мы можем точно видеть, какой ряд нашего Datatable вызвал сбой и почему.

5. Дразнящий

5.1. Что такое издевательство?

Пересмешка - это способ изменить поведение класса, с которым сотрудничает наш тестируемый сервис. Это полезный способ тестирования бизнес-логики изолированно от ее зависимостей.

Классическим примером этого будет замена класса, который делает сетевой вызов, чем-то, что просто притворяется. Для более подробного объяснения стоит прочитатьthis article.

5.2. Издевательство с использованием Спока

У Spock есть собственный фреймворк mocking, в котором используются интересные концепции, представленные в JVM Groovy. Во-первых, давайте создадим экземплярMock:

PaymentGateway paymentGateway = Mock()

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

def paymentGateway = Mock(PaymentGateway)

Теперь всякий раз, когда мы вызываем метод в нашем макетеPaymentGateway,, будет дан ответ по умолчанию, без вызова реального экземпляра:

when:
    def result = paymentGateway.makePayment(12.99)

then:
    result == false

Член для этогоlenient mocking. Это означает, что фиктивные методы, которые не были определены, будут возвращать разумные значения по умолчанию, а не генерировать исключение. Это спроектировано в Споке, чтобы сделать насмешки и, следовательно, тесты менее хрупкими.

5.3. Вызов метода заглушки наMocks

Мы также можем настроить методы, вызываемые на нашем макете, чтобы определенным образом реагировать на разные аргументы. Давайте попробуем заставить наш макетPaymentGateway возвращатьtrue, когда мы производим платеж в размере20:

given:
    paymentGateway.makePayment(20) >> true

when:
    def result = paymentGateway.makePayment(20)

then:
    result == true

Здесь интересно то, как Спок использует перегрузку операторов Groovy, чтобы заглушить вызовы методов. В Java мы должны вызывать реальные методы, что, вероятно, означает, что полученный код более многословен и потенциально менее выразителен.

А теперь давайте попробуем еще несколько типов заглушек.

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

paymentGateway.makePayment(_) >> true

Если бы мы хотели чередовать разные ответы, мы могли бы предоставить список, для которого каждый элемент будет возвращаться в последовательности:

paymentGateway.makePayment(_) >>> [true, true, false, true]

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

5.4. верификация

Еще одна вещь, которую мы могли бы сделать с mocks, - это утверждать, что к ним были вызваны различные методы с ожидаемыми параметрами. Другими словами, мы должны проверить взаимодействие с нашими издевательствами.

Типичный вариант использования для проверки, если метод в нашем макете имеет тип возвратаvoid. В этом случае, поскольку у нас нет результата, с которым мы могли бы работать, мы не можем предположить поведение, которое мы можем протестировать с помощью тестируемого метода. Как правило, если что-то было возвращено, то тестируемый метод мог бы работать с этим, и результат этой операции будет тем, что мы утверждаем.

Давайте попробуем проверить, вызывается ли метод с возвращаемым типом void:

def "Should verify notify was called"() {
    given:
        def notifier = Mock(Notifier)

    when:
        notifier.notify('foo')

    then:
        1 * notifier.notify('foo')
}

Спок снова использует перегрузку Groovy-оператора. Умножая наш вызов метода mocks на один, мы говорим, сколько раз мы ожидаем, что он был вызван.

Если бы наш метод вообще не вызывался или альтернативно не вызывался столько раз, сколько мы указали, тогда наш тест не дал бы нам информативное сообщение об ошибке Спока. Давайте докажем это, предположив, что он будет вызван дважды:

2 * notifier.notify('foo')

После этого давайте посмотрим, как выглядит сообщение об ошибке. Мы сделаем это как обычно; это довольно информативно:

Too few invocations for:

2 * notifier.notify('foo')   (1 invocation)

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

2 * notifier.notify(_)

Или, если мы хотим убедиться, что он не вызывается с определенным аргументом, мы могли бы использовать оператор not:

2 * notifier.notify(!'foo')

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

6. Заключение

В этой статье мы кратко рассмотрели тестирование со Споком.

Мы продемонстрировали, как, используя Groovy, мы можем сделать наши тесты более выразительными, чем типичный стек JUnit. Мы объяснили структуруspecifications иfeatures.

И мы показали, насколько легко выполнять тестирование на основе данных, а также насколько легко использовать имитацию и утверждения с помощью встроенных функций Spock.

Реализацию этих примеров можно найти вover on GitHub. Это проект, основанный на Maven, поэтому его легко запустить как есть.