Перейти к основному содержимому

Введение в тестирование с помощью Spock и Groovy

· 12 мин. чтения

Задача: Сумма двух чисел

Напишите функцию twoSum. Которая получает массив целых чисел nums и целую сумму target, а возвращает индексы двух чисел, сумма которых равна target. Любой набор входных данных имеет ровно одно решение, и вы не можете использовать один и тот же элемент дважды. Ответ можно возвращать в любом порядке...

ANDROMEDA

1. Введение

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

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

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

2. Зависимость от Maven

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

<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<version>1.0-groovy-2.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>2.4.7</version>
<scope>test</scope>
</dependency>

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

<plugin>
<groupId>org.codehaus.gmavenplus</groupId>
<artifactId>gmavenplus-plugin</artifactId>
<version>1.5</version>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
</plugin>

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

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

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

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

class FirstSpecification extends Specification {

}

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

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

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

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

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

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

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

3.2. Блоки

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

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

// When
int result = 2 + 2;

// Then
assertTrue(result == 4)
}

Спок решает эту проблему с помощью блоков. Блоки — это родной способ Spock разбить этапы нашего теста с помощью меток. Они дают нам ярлыки для данного, когда тогда и больше:

  1. Настройка (псевдоним от Given) — здесь мы выполняем любую настройку, необходимую перед запуском теста. Это неявный блок, код которого вообще не входит в какой-либо блок и становится его частью.
  2. Когда — здесь мы стимулируем то, что тестируется. Другими словами, когда мы вызываем наш тестируемый метод
  3. Тогда – вот где утверждения принадлежат. В Spock они оцениваются как простые логические утверждения, которые будут рассмотрены позже.
  4. Ожидание — это способ выполнения нашего стимула и утверждения в одном и том же блоке. В зависимости от того, что мы находим более выразительным, мы можем использовать или не использовать этот блок.
  5. Очистка — здесь мы удаляем все ресурсы тестовых зависимостей, которые в противном случае остались бы позади. Например, мы можем захотеть удалить любые файлы из файловой системы или удалить тестовые данные, записанные в базу данных.

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

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 для утверждений

Внутри блоков then и expect утверждения являются неявными .

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

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 предлагает более простые способы создания списков. Мы можем просто объявить наши элементы с помощью квадратных скобок, и внутри будет создан список .

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

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

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

Condition not satisfied:

list == [1, 3, 4]
| |
| false
[2, 3, 4]
<Click to see difference>

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

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

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

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

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
}

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

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

4.1. Что такое тестирование, управляемое данными?

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

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

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

@RunWith(Parameterized.class)
public class FibonacciTest {
@Parameters
public static Collection<Object[]> 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

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

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

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

5. Издевательство

5.1. Что такое насмешка?

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

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

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

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

PaymentGateway paymentGateway = Mock()

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

def paymentGateway = Mock(PaymentGateway)

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

when:
def result = paymentGateway.makePayment(12.99)

then:
result == false

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

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

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

given:
paymentGateway.makePayment(20) >> true

when:
def result = paymentGateway.makePayment(20)

then:
result == true

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

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

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

paymentGateway.makePayment(_) >> true

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

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

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

5.4. Проверка

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

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

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

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

when:
notifier.notify('foo')

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

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

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

2 * notifier.notify('foo')

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

Too few invocations for:

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

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

2 * notifier.notify(_)

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

2 * notifier.notify(!'foo')

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

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

В этой статье мы дали краткий обзор тестирования с помощью Spock.

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

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

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