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

Руководство по расширениям Spock

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

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
def "I won't be executed"() {
expect:
true
}

Спок не будет выполнять этот тестовый метод. И большинство 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({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"() {}

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

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

3.4. @Требует

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

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

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

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

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 :

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

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

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

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

3.7. @Тайм-аут

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

@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. @Повторить попытку

Иногда у нас могут быть недетерминированные интеграционные тесты. Это может привести к сбою в некоторых запусках по таким причинам, как асинхронная обработка или в зависимости от ответа других 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 S :

@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("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("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
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
}

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

Чтобы увидеть, как это работает, и попрактиковаться, давайте создадим простой тест, который генерирует исключение 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...

Установив для этого свойства значение 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 . Мы можем определить правила include так же, как и exclude .

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

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

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

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

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

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

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

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

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

5. Вывод

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

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

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