«1. Обзор

В этом уроке мы рассмотрим расширения Spock.

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

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

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

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

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

<dependency>
    <groupId>org.spockframework</groupId>
    <artifactId>spock-core</artifactId>z
    <version>1.3-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>

3. Расширения на основе аннотаций

Большинство встроенных расширений Spock основаны на аннотациях .

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

3.1. @Ignore

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

Мы можем использовать @Ignore на уровне метода, чтобы пропустить один метод спецификации:

@Ignore
def "I won't be executed"() {
    expect:
    true
}

Spock не будет выполнять этот тестовый метод. И большинство IDE помечают тест как пропущенный.

Кроме того, мы можем использовать @Ignore на уровне класса:

@Ignore
class IgnoreTest extends Specification

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

@Ignore("probably no longer needed")

3.2. @IgnoreRest

Точно так же мы можем игнорировать все спецификации, кроме одной, которую мы можем пометить аннотацией @IgnoreRest:

def "I won't run"() { }

@IgnoreRest
def 'I will run'() { }

def "I won't run too"() { }

3.3. @IgnoreIf

Иногда мы хотели бы условно игнорировать тест или два. В этом случае мы можем использовать @IgnoreIf, который принимает предикат в качестве аргумента:

@IgnoreIf({System.getProperty("os.name").contains("windows")})
def "I won't run on windows"() { }

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

    os †Информация об операционной системе (см. spock.util.environment.OperatingSystem). jvm — информация о JVM (см. spock.util.environment.Jvm). sys — свойства системы на карте. env — переменные среды на карте.

Мы можем переписать предыдущий пример, используя свойство os. На самом деле, это класс spock.util.environment.OperatingSystem с некоторыми полезными методами, такими как, например, isWindows():

@IgnoreIf({ os.isWindows() })
def "I'm using Spock helper classes to run only on windows"() {}

Обратите внимание, что Spock использует System.getProperty(…) под капотом. Основная цель — предоставить понятный интерфейс, а не определение сложных правил и условий.

Также, как и в предыдущих примерах, мы можем применить аннотацию @IgnoreIf на уровне класса.

3.4. @Requires

Иногда проще инвертировать нашу логику предиката из @IgnoreIf. В этом случае мы можем использовать @Requires:

@Requires({ System.getProperty("os.name").contains("windows") })
def "I will run only on Windows"()

Таким образом, в то время как @Requires запускает этот тест только в том случае, если используется ОС Windows, @IgnoreIf, используя тот же предикат, запускает тест только в том случае, если ОС это не винда.

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

3.5. @PendingFeature

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

Это хороший пример использования @PendingFeature:

@PendingFeature
def 'test for not implemented yet feature. Maybe in the future it will pass'()

Существует одно основное различие между @Ignore и @PendingFeature. В @PedingFeature тесты выполняются, но любые сбои игнорируются.

Если тест, помеченный @PendingFeature, завершится без ошибок, то будет сообщено об ошибке, чтобы напомнить об удалении аннотации.

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

3.6. @Stepwise

Мы можем выполнять методы спецификации в заданном порядке с помощью аннотации @Stepwise:

def 'I will run as first'() { }

def 'I will run as second'() { }

В общем, тесты должны быть детерминированными. Одно не должно зависеть от другого. Вот почему нам следует избегать использования аннотации @Stepwise.

Но если нам нужно, мы должны знать, что @Stepwise не переопределяет поведение @Ignore, @IgnoreRest или @IgnoreIf. Мы должны быть осторожны с комбинацией этих аннотаций с @Stepwise.

«3.7. @Timeout

Мы можем ограничить время выполнения единственного метода спецификации и выйти из строя раньше:

@Timeout(1)
def 'I have one second to finish'() { }

Обратите внимание, что это время ожидания для одной итерации, не считая времени, затрачиваемого на методы фиксации.

По умолчанию spock.lang.Timeout использует секунды в качестве базовой единицы времени. Но мы можем указать другие единицы измерения времени:

@Timeout(value = 200, unit = TimeUnit.SECONDS)
def 'I will fail after 200 millis'() { }

@Timeout на уровне класса имеет тот же эффект, что и применение его к каждому методу объекта по отдельности:

@Timeout(5)
class ExampleTest extends Specification {

    @Timeout(1)
    def 'I have one second to finish'() {

    }

    def 'I will have 5 seconds timeout'() {}
}

Использование @Timeout для одного метода спецификации всегда переопределяет уровень класса.

3.8. @Retry

Иногда у нас могут быть недетерминированные интеграционные тесты. Это может привести к сбою в некоторых запусках по таким причинам, как асинхронная обработка или в зависимости от ответа других HTTP-клиентов. Более того, удаленный сервер со сборкой и CI выйдет из строя и заставит нас запускать тесты и собирать заново.

Чтобы избежать этой ситуации, мы можем использовать аннотацию @Retry на уровне метода или класса, чтобы повторить неудачные тесты:

@Retry
def 'I will retry three times'() { }

По умолчанию он будет повторять попытку три раза.

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

@Retry(exceptions = [RuntimeException])
def 'I will retry only on RuntimeException'() { }

Или когда есть конкретное сообщение об исключении:

@Retry(condition = { failure.message.contains('error') })
def 'I will retry with a specific message'() { }

Очень полезна повторная попытка с задержкой:

@Retry(delay = 1000)
def 'I will retry after 1000 millis'() { }

И, наконец, как почти всегда , мы можем указать повтор на уровне класса:

@Retry
class RetryTest extends Specification

3.9. @RestoreSystemProperties

Мы можем манипулировать переменными среды с помощью @RestoreSystemProperties.

Эта аннотация при применении сохраняет текущее состояние переменных и восстанавливает их впоследствии. Он также включает методы настройки или очистки:

@RestoreSystemProperties
def 'all environment variables will be saved before execution and restored after tests'() {
    given:
    System.setProperty('os.name', 'Mac OS')
}

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

3.10. Удобные для человека заголовки

Мы можем добавить удобный для человека заголовок теста, используя аннотацию @Title:

@Title("This title is easy to read for humans")
class CustomTitleTest extends Specification

Точно так же мы можем добавить описание спецификации с помощью аннотации @Narrative и многострочного Groovy. Строка:

@Narrative("""
    as a user
    i want to save favourite items 
    and then get the list of them
""")
class NarrativeDescriptionTest extends Specification

3.11. @See

Чтобы связать одну или несколько внешних ссылок, мы можем использовать аннотацию @See:

@See("https://example.org")
def 'Look at the reference'()

Чтобы передать более одной ссылки, мы можем использовать операнд Groovy [] для создания списка:

@See(["https://example.org/first", "https://example.org/first"])
def 'Look at the references'()

3.12. @Issue

Мы можем обозначить, что метод признаков относится к проблеме или нескольким проблемам:

@Issue("https://jira.org/issues/LO-531")
def 'single issue'() {

}

@Issue(["https://jira.org/issues/LO-531", "http://jira.org/issues/LO-123"])
def 'multiple issues'()

3.13. @Subject

И, наконец, мы можем указать, какой класс является тестируемым, с помощью @Subject:

@Subject
ItemService itemService // initialization here...

Сейчас это только для информационных целей.

4. Настройка расширений

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

Обычно мы создаем файл конфигурации в Groovy, который называется, например, SpockConfig.groovy.

Конечно, Споку нужно найти наш конфигурационный файл. Прежде всего, он считывает пользовательское местоположение из системного свойства spock.configuration, а затем пытается найти файл в пути к классам. Если он не найден, он отправляется в место в файловой системе. Если он все еще не найден, он ищет SpockConfig.groovy в пути к классам выполнения теста.

В конце концов, Spock переходит в домашний каталог пользователя Spock, который является просто каталогом .spock в нашем домашнем каталоге. Мы можем изменить этот каталог, установив системное свойство spock.user.home или переменную среды SPOCK_USER_HOME.

Для наших примеров мы создадим файл SpockConfig.groovy и поместим его в путь к классам (src/test/resources/SpockConfig.Groovy).

4.1. Фильтрация трассировки стека

Используя файл конфигурации, мы можем фильтровать (или нет) трассировку стека:

runner {
    filterStackTrace false
}

Значение по умолчанию — true.

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

def 'stacktrace'() {
    expect:
    throw new RuntimeException("blabla")
}

Когда для filterStackTrace установлено значение false, мы увидим в выводе:

java.lang.RuntimeException: blabla

  at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
  at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
  at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
  at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
  at org.codehaus.groovy.reflection.CachedConstructor.invoke(CachedConstructor.java:83)
  at org.codehaus.groovy.runtime.callsite.ConstructorSite$ConstructorSiteNoUnwrapNoCoerce.callConstructor(ConstructorSite.java:105)
  at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCallConstructor(CallSiteArray.java:60)
  at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callConstructor(AbstractCallSite.java:235)
  at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callConstructor(AbstractCallSite.java:247)
  // 34 more lines in the stack trace...

By установив для этого свойства значение true, мы получим:

java.lang.RuntimeException: blabla

  at extensions.StackTraceTest.stacktrace(StackTraceTest.groovy:10)

Хотя имейте в виду, иногда полезно увидеть полную трассировку стека.

4.2. Условные функции в файле конфигурации Spock

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

Мы можем добавить простое условие, основанное, например, на переменных среды:

if (System.getenv("FILTER_STACKTRACE") == null) {   
    filterStackTrace false
}

Файл конфигурации Spock является файлом Groovy, поэтому он может содержать фрагменты кода Groovy.

4.3. Префикс и URL в @Issue

Ранее мы говорили об аннотации @Issue. Мы также можем настроить это с помощью файла конфигурации, определив общую часть URL-адреса с помощью issueUrlPrefix.

Другое свойство — issueNamePrefix. Затем каждому значению @Issue предшествует свойство issueNamePrefix.

Нам нужно добавить эти два свойства в отчет:

report {
    issueNamePrefix 'Bug '
    issueUrlPrefix 'https://jira.org/issues/'
}

4.4. Оптимизация порядка выполнения

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

Основываясь на этом знании, Спок сначала запустит функции, которые не удались при последнем запуске. Во-первых, он будет выполнять спецификации, которые не сработали, более последовательно. Кроме того, самые быстрые спецификации будут работать первыми.

Это поведение может быть включено в файле конфигурации. Чтобы включить оптимизатор, мы используем свойство optimizeRunOrder:

runner {
  optimizeRunOrder true
}

По умолчанию оптимизатор порядка выполнения отключен.

4.5. Включение и исключение спецификаций

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

Мы можем просто исключить набор тестов из класса TimeoutTest, используя свойство exclude:

import extensions.TimeoutTest

runner {
    exclude TimeoutTest
}

TimeoutTest и все его подклассы будут исключены. Если бы TimeoutTest была аннотацией, примененной к классу спецификации, то эта спецификация была бы исключена.

Мы можем указать аннотации и базовые классы отдельно:

import extensions.TimeoutTest
import spock.lang.Issue
    exclude {
        baseClass TimeoutTest
        annotation Issue
}

В приведенном выше примере будут исключены тестовые классы или методы с аннотацией @Issue, а также TimeoutTest или любой из его подклассов.

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

4.6. Создание отчета

На основе результатов тестирования и ранее известных аннотаций мы можем создать отчет с помощью Spock. Кроме того, этот отчет будет содержать такие значения, как @Title, @See, @Issue и @Narrative.

Мы можем включить генерацию отчета в конфигурационном файле. По умолчанию он не создает отчет.

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

report {
    enabled true
    logFileDir '.'
    logFileName 'report.json'
    logFileSuffix new Date().format('yyyy-MM-dd')
}

Вышеуказанные свойства:

    enabled — должен или не должен генерировать отчет logFileDir — каталог отчета logFileName — “ имя отчета logFileSuffix – суффикс для каждого сгенерированного базового имени отчета, разделенный дефисом

Когда мы устанавливаем enable в true, то обязательно установить свойства logFileDir и logFileName. logFileSuffix является необязательным.

Мы также можем установить их все в системных свойствах: enable, spock.logFileDir, spock.logFileName и spock.logFileSuffix.

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

В этой статье мы описали наиболее распространенные расширения Spock.

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

Реализацию всех наших примеров можно найти в нашем проекте Github.