Начинаем с Cucumber

23 декабря 2021

Software Development Life Cycle (SDLC - жизненный цикл разработки программного обеспечения) обычно включает людей двух типов: бизнес-профессионалов и инженеров. Поскольку их опыт находится в разных областях, бизнес-требования легко могут быть неправильно поняты или выражены нечетко, и конечный продукт может не соответствовать бизнес-потребностям. Behavior-Driven Development (BDD) - это процесс разработки программного обеспечения, который поощряет:

  • Сотрудничество всей команды.
  • Использование понятного для всех языка (ubiquitous language) для определения поведения системы, то есть спецификации.
  • Использование автоматических тестов для проверки (валидации) системы на соответствие спецификации.

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

Значит, спецификация, написанная в виде обычного текста, может быть выбрана платформой тестирования и затем выполнена? Что можно придумать, похожее на JUnit (JUnit – фреймворк для модульного тестирования программного обеспечения на языке Java)? Рассмотрим Cucumber! Cucumber читает спецификации и проверяет, что делает программное обеспечение, в соответствии с тем, что указано в спецификации. Это поможет вам интегрировать BDD в ваш SDLC. Cucumber доступен для многих языков программирования, а спецификации могут быть написаны на разных разговорных языках.

В этом туториале вы:

  • Примените Gherkin для определения характеристик.
  • Сможете интегрировать Cucumber в приложение Spring Boot.
  • Ознакомитесь с библиотеками, такими как Hamcrest и Rest Assured, для проверки спецификаций.
  • Узнаете, как разделить состояние между действиями.
  • Примените несколько потоков для параллельного выполнения тестов.
  • Узнаете, как бороться с нестабильными тестами.
  • Создадите отчеты об испытаниях.

Вы будете работать с приложением под названием artikles, созданным с помощью Spring Boot и Kotlin.

Заметка

В этой статье предполагается, что вы знакомы с основами Spring Boot и Hibernate.

 

Приступим

Щелкните на Download Materials, чтобы загрузить начальный проект. Запустите IntelliJ IDEA и выберите Open…. Затем перейдите к стартовому проекту и откройте его.

Скомпилируйте и запустите приложение. Вы обнаружите, что сервер работает на порту 8080.

Теперь создайте первую статью:

curl -X POST http://localhost:8080/articles -d '{"title":"Test","body":"Hello, World!"}' -H "Content-Type:application/json"

И отправьте запрос GET request:

curl http://localhost:8080/articles

В ответ вы получите только что созданную статью. Здорово! Теперь, когда ваше приложение запущено, запустите браузер и перейдите на localhost:8080/h2-console. Войдите в систему, используя следующие конфигурации:

Вы можете найти эти конфигурации в application.properties, расположенном в src/main/resources. Это приложение использует базу данных H2 в памяти. Имейте в виду, что остановка и перезапуск приложения приведет к сбросу базы данных. В стартовом проекте пока нет тестовых примеров. В следующих разделах вы узнаете, как интегрировать тесты в этот проект. А сейчас посмотрим Gherkin!

 

Gherkin

Спецификация исполняемого файла на самом деле не является «простым» текстом. Она написана в структурированном формате под названием Gherkin. Gherkin предоставляет набор грамматических правил и ключевых слов для описания спецификации. Спецификации находятся в файлах *.feature и содержат один или несколько сценариев. Каждый сценарий содержит серию шагов, которые Cucumber выполняет и проверяет на соответствие ожиданиям бизнеса.

Типичная спецификация выглядит так:

Feature: Bank transfer.

  Scenario: Money should be transferred from applicant account to beneficiary account.
	Given Account "001" has balance of "$100".
	And Account "002" has balance of "$1000".
	When Amount of "$50" is transferred from account "001" to account "002".
	Then Account "001" should have balance of "$50".
	And Account "002" should have balance of "$1050".

Gherkin предоставляет:

  • Feature- группировать связанные сценарии.
  • Scenario или Example, чтобы сгруппировать серию шагов.
  • Given (дано), When (когда), Then (затем), And (и) и But (но) для описания шагов. Эти шаги выполняются последовательно.
  • Scenario Outline (схему сценария) для многократного запуска одного и того же Scenario (сценария) с использованием различных комбинаций входных данных.

Вы можете предоставить список входных данных для определения шага с помощью Data Tables (таблиц данных):

| account |   balance   |
|   001   |     $100    |
|   002   |     $150    |
|   004   |     $1000   |

Обратитесь к документации Gherkin (Gherkin documentation), чтобы узнать о ключевых словах и их использовании. Далее вы узнаете о Cucumber.

Cucumber

Cucumber связывает шаги Gherkin с определением шага (step definition). Определение шага - это метод, помеченный одним из ключевых слов шага: (@Given, @When, @Then или @But). Он содержит либо регулярное выражение (Regular Expression), либо Cucumber Expression, которое связывает метод с шагами Gherkin.

Определение шага для описанного выше сценария может выглядеть так:

import io.cucumber.java.en.*

class StepsDefinition {

  @Given("Account {string} has balance of {string}")
  fun setUpAccountWithBalance(account: String, balance: String) {
    // Account "001" has balance of "$100".
    // Account "002" has balance of "$1000".

    val money: Money = Money.from(balance)
    // setup account with [money]
  }

  @When("Amount of {string} is transferred from account {string} to account string")
  fun transferAmount(balance: String, fromAccountNumber: String, toAccountNumber: String) {
    // Amount of "$50" is transferred from account "001" to account "002".

    val fromAccount = getAccount(fromAccountNumber)
    val toAccount = getAccount(toAccountNumber)
    // Transfer balance
    TransferService.transfer(from=fromAccount, to=toAccount, amount=balance)
  }

  @Then("Account {string} should have balance of {string}.")
  fun validateAmount(accountNumber: String, balance: String) {
    // Account "001" should have balance of "$50".
    // Account "002" should have balance of "$1050".

    val account =  getAccount(accountNumber)
    assertEquals(balance, account.balance)
  }

  // ...
}

Методы определения шага либо инициализируют, либо устанавливают состояние, либо проверяют текущее состояние системы.

Cucumber Expressions поддерживают основные типы параметров, такие как {int}, {float}, {string}, {biginteger}, {double}, {long} и {word}. Вы можете узнать о них больше в документации Cucumber Expressions (Cucumber Expressions documentation).

Теперь, когда вы знаете о Cucumber, вы научитесь интегрировать его в проект Spring Boot.

 

Настройка Cucumber

Сначала добавьте в build.gradle следующие зависимости:

// 1
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("io.rest-assured:rest-assured:4.4.0")

// 2
testImplementation("io.cucumber:cucumber-java:6.10.4")
// 3
testImplementation("io.cucumber:cucumber-spring:6.10.4")
// 4
testImplementation("io.cucumber:cucumber-junit-platform-engine:6.10.4")

// 5
testRuntimeOnly("org.junit.platform:junit-platform-console")

Итак, что здесь происходит?

1. Cucumber категорически настроен по отношению к библиотеке assertion, поэтому вы можете использовать ту, которая вам больше нравится. Вы будете использовать Rest Assured для имитации конечных точек HTTP и Hamcrest для подтверждения ответа HTTP.

2. Здесь добавляются основные классы и аннотации Cucumber в область тестирования.

3. cucumber-spring требуется, потому что приложение должно запускаться и быть готовым принимать HTTP-запросы перед выполнением любого из шагов.

4. cucumber-junit-platform-engine - это механизм, который JUnit Console Launcher использует для выполнения сценария Cucumber.

5. Используется Console Launcher (средство запуска консоли) для запуска платформы JUnit и выполнения тестовых примеров с консоли.

В build.gradle замените блок tasks.withType следующим:

tasks {
  val consoleLauncherTest by registering(JavaExec::class) {
    dependsOn(testClasses)
    classpath = sourceSets["test"].runtimeClasspath
    mainClass.set("org.junit.platform.console.ConsoleLauncher")
    args("--include-engine", "cucumber")
    args("--details", "tree")
    args("--scan-classpath")
    // Pretty prints the output in console
    systemProperty("cucumber.plugin", "pretty")
    // Hides Cucumber ads
    systemProperty("cucumber.publish.quiet", true)
  }

  test {
    dependsOn(consoleLauncherTest)
    exclude("**/*")
  }
}

Этот код настроил JUnit Console Launcher с ядром платформы (cucumber-junit-platform-engine) для выполнения сценариев Cucumber.

Выполните синхронизацию Gradle и выполните эту задачу, используя тест ./gradlew test на машинах Unix или тест gradlew.bat test в Windows. Вы должны увидеть запуск задачи gradle, а затем блок текста, который выглядит примерно так:

Test run finished after 147 ms
[         1 containers found      ]
[         0 containers skipped    ]
[         1 containers started    ]
[         0 containers aborted    ]
[         1 containers successful ]
[         0 containers failed     ]
[         0 tests found           ]
[         0 tests skipped         ]
[         0 tests started         ]
[         0 tests aborted         ]
[         0 tests successful      ]
[         0 tests failed          ]

Наконец, перейдите в Preferences > Plugins и убедитесь, что вы установили необходимые плагины для Cucumber и включили их в IntelliJ. Возможно, вам придется перезапустить IntelliJ после установки плагинов. Эти плагины помогают значительно упростить процесс тестирования при использовании Cucumber.

 

Теперь вы готовы интегрировать Cucumber в Spring Boot. Перед этим совершите краткую экскурсию по Rest Assured.

Rest Assured 

Rest Assured значительно упрощает тестирование служб REST. Как упоминалось ранее, вы будете использовать его для моделирования контроллеров REST, а затем проверить ответ.

Его API-интерфейсы следуют структуре Given-When-Then (дано-когда-тогда). Простой тест с использованием Rest Assured выглядит так:

// 1
val requestSpec: RequestSpecification = RestAssured
    .given()
    .contentType(ContentType.JSON)
    .accept(ContentType.JSON)
    .pathParam("id", 1)

// 2 
val response: Response = requestSpec
    .`when`()
    .get("https://jsonplaceholder.typicode.com/todos/{id}")

response
.then() // 3
.statusCode(200) // 4
.body("id", Matchers.equalTo(1)) // 4
.body("title", Matchers.notNullValue())
//...

Здесь:

1. Вы предоставили параметры пути и заголовки и создали RequestSpecification, используя их. Это раздел “given”.

2. Используя RequestSpecification, созданный выше, вы отправили запрос GET request на https://jsonplaceholder.typicode.com/todos/1?id=1. В случае успеха он возвращает ответ со следующей схемой:

{
  "userId": 1,
  "id": 1,
  "title": "delectus aut autem",
  "completed": false
}

3. Получив ответ HTTP, проверьте его соответствие вашим ожиданиям. then() возвращает ValidatableResponse, который предоставляет fluent interface, в котором вы можете связать свои методы assertion.

4. Вы подтвердили, соответствуют ли код состояния и HTTP-ответ ожидаемым с помощью Hamcrest Matchers.

Rest Assured может сделать гораздо больше. Обратитесь к документации Rest Assured documentation , чтобы узнать больше. Далее вы увидите это в действии.

 

Cucumber и Spring Boot

Spring Boot требуется несколько секунд, прежде чем он сможет начать обслуживание HTTP-запросов. Таким образом, вы должны предоставить контекст приложения для использования Cucumber.

Создайте класс SpringContextConfiguration.kt в src/test/kotlin/com/raywenderlich/artikles и вставьте фрагмент кода ниже:

import org.springframework.boot.test.context.SpringBootTest
import io.cucumber.spring.CucumberContextConfiguration

@SpringBootTest(
    classes = [ArtiklesApplication::class],
    webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
)
@CucumberContextConfiguration
class SpringContextConfiguration

Здесь вы настроили Spring Boot для запуска на случайном порту до начала выполнения теста.

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

import org.springframework.boot.web.server.LocalServerPort
import io.cucumber.java.*
import io.cucumber.java.en.*
import io.restassured.RestAssured

class ArticleStepDefs : SpringContextConfiguration() {

  @LocalServerPort // 1
  private var port: Int? = 0

  @Before // 2
  fun setup(scenario: Scenario) {
    RestAssured.baseURI = "http://localhost:$port" // 3
    RestAssured.enableLoggingOfRequestAndResponseIfValidationFails()
  }
}

Итак, что здесь происходит?

1. В текущей конфигурации Spring Boot начинает прослушивать HTTP-запросы на случайном порту. @LocalServerPort связывает этот случайно назначенный порт с port.

2. @Before - это scenario hook, который Cucumber вызывает один раз перед каждым сценарием. Хотя scenario hook @After запускается после каждого сценария. Точно так же @BeforeStep и @AfterStep - это перехватчики шагов, которые запускаются до и после каждого шага соответственно. Вы можете использовать их для инициализации или очистки ресурсов или состояния.

3. Создаёте URI приложения, используя динамически выделенный порт, и используете его для настройки базового URL-адреса Rest Assured.

Теперь вы готовы сделать первый шаг.

 

Feature: Create Article

Добавьте новый каталог с именем create в /src/test/resources/com/raywenderlich/artikles/. Затем создайте новый файл с именем create-article.feature и вставьте следующий фрагмент:

Feature: Create Article.

  Scenario: As a user, I should be able to create new article.
  New article should be free by default and the created article should be viewable.
    Given Create an article with following fields
      | title | Cucumber                                      |
      | body  | Write executable specifications in plain text |

Вы определили сценарий в Gherkin. Он содержит только один шаг. Позже вы добавите другие шаги. Вы будете использовать предоставленные поля (в качестве полезной нагрузки HTTP) для создания статьи (отправьте запрос POST request).

Установив курсор на шаг, откройте контекстное меню. Выберите Create step definition. Затем выберите файл, в котором вы создадите соответствующий метод определения шага. В данном случае это ArticleStepDefs.kt.

 

Вы также можете вручную создать метод внутри ArticleStepDefs. Убедитесь, что шаг Gherkin соответствует выражению Cucumber. Для “Create an article with following fields” метод определения шага такой:

@Given("Create an article with following fields")
fun createAnArticleWithFollowingFields(payload: Map) {
}

Вышеупомянутый метод получает содержимое таблицы данных в виде payload (полезной нагрузки). Вы можете отправить его как полезную нагрузку HTTP запроса POST по адресу /article.

Наконец, запустите тест с помощью интерфейса командной строки или IDE. В IntelliJ создайте новую конфигурацию Cucumber Java со следующими значениями:

Main class: io.cucumber.core.cli.Main
Glue: com.raywenderlich.artikles
Program arguments: --plugin org.jetbrains.plugins.cucumber.java.run.CucumberJvm5SMFormatter

Выберите будущий файл для выполнения в Feature or folder path. Конфигурация будет выглядеть как на изображении ниже.

 

Нажмите кнопку Run (выполнить). Вы должны увидеть прохождение теста.

 

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

State (состояние)

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

Поскольку вы тестируете с точки зрения пользователя или HTTP-клиента, выстраивайте состояние вокруг таких HTTP-конструкций, как запрос, ответ, полезная нагрузка и переменная пути (differentiator).

Создайте StateHolder.kt в src/test/kotlin/com/raywenderlich/artikles. Вставьте фрагмент ниже:

import io.restassured.response.Response
import io.restassured.specification.RequestSpecification
import java.lang.ThreadLocal

object StateHolder {

  private class State {
    var response: Response? = null
    var request: RequestSpecification? = null
    var payload: Any? = null

    /**
     * The value that uniquely identifies an entity
     */
    var differentiator: Any? = null
  }

  private val store: ThreadLocal = ThreadLocal.withInitial { State() }

  private val state: State
    get() = store.get()
}

store - это экземпляр ThreadLocal. Этот класс поддерживает карту между текущим потоком и экземпляром State. Вызов get() в store возвращает экземпляр State, связанный с current thread (текущим потоком). Это важно при выполнении тестов в нескольких потоках. Подробнее об этом вы узнаете позже.

Вам нужно выставить атрибуты State. Вставьте следующие методы в StateHolder:

fun setDifferentiator(value: Any) {
  state.differentiator = value
}

fun getDifferentiator(): Any? {
  return state.differentiator
}

fun setRequest(request: RequestSpecification) {
  state.request = request
}

fun getRequest(): RequestSpecification {
  var specs = state.request
  if (specs == null) {
    // 1
    specs = given()
        .contentType(ContentType.JSON)
        .accept(ContentType.JSON)
    setRequest(specs)
    return specs
  }
  return specs
}

fun setPayload(payload: Any): RequestSpecification {
  val specs = getRequest()
  state.payload = payload
  // 2
  return specs.body(payload)
}

fun getPayloadOrNull(): Any? {
  return state.payload
}

fun getPayload(): Any {
  return getPayloadOrNull()!!
}

fun setResponse(value: Response) {
  state.response = value
}

fun getResponse() = getResponseOrNull()!!

fun getResponseOrNull() = state.response

// 3
fun clear() {
  store.remove()
}

Эти методы (геттеры) соответствуют соглашениям языка Kotlin getX() и getXOrNull(). Следует отметить следующие нетипичные:

1. getRequest() возвращает минимально сконфигурированную RequestSpecification, если State текущего потока не имеет связанного экземпляра RequestSpecification. Вспомните раздел “Given” в Given-When-Then от Rest Assured.

2. setPayload() устанавливает HTTP-тело RequestSpecification, возвращаемого getRequest(). Это полезно для запросов PUT и POST.

3. clear() удаляет экземпляр State, связанный с текущим потоком.

Импорт выглядит так:

import io.restassured.RestAssured.given
import io.restassured.http.ContentType
import io.restassured.response.Response
import io.restassured.response.ValidatableResponse
import io.restassured.specification.RequestSpecification

В том же классе добавьте эти вспомогательные методы:

// 1
fun  getPayloadAs(klass: Class): T {
  return klass.cast(getPayload())
}

// 1
fun getPayloadAsMap(): Map {
  return getPayloadAs(Map::class.java)
}

// 2
fun getValidatableResponse(): ValidatableResponse {
  return getResponse().then()
}

fun  extractPathValueFromResponse(path: String): T? {
  return extractPathValueFrom(path, getValidatableResponse())
}

// 3
private fun  extractPathValueFrom(path: String, response: ValidatableResponse): T? {
  return response.extract().body().path(path)
}

Эти методы:

1. Преобразует полезную нагрузку HTTP в другой класс.

2. Предоставляет API для подтверждения Rest Assured Response.

3. Извлекает значение из HTTP-ответа, сопоставленного с путем.

Найдите минутку, чтобы изучить этот класс. Затем вы увидите эти методы в действии.

 

Создание HTTP-клиента

Чтобы смоделировать контроллер REST, выполните HTTP-вызовы к конечной точке /articles. Используйте для этого Rest Assured.

Создайте HttpUtils.kt в src/test/kotlin/com/raywenderlich/artikles и вставьте фрагмент ниже:

import io.restassured.response.Response

fun  withPayload(payload: T, block: () -> Response?) {
  StateHolder.setPayload(payload)
  block()
}

object HttpUtils {

  private fun executeAndSetResponse(block: () -> Response): Response {
    val response = block()
    StateHolder.setResponse(response)
    return StateHolder.getResponse()
  }

  fun executePost(url: String): Response {
    return executeAndSetResponse {
      StateHolder.getRequest().post(url)
    }
  }

  fun executePut(url: String): Response {
    return executeAndSetResponse {
      StateHolder.getRequest().put(url)
    }
  }

  fun executeGet(url: String): Response {
    return executeAndSetResponse {
      StateHolder.getRequest().get(url)
    }
  }

  fun executeDelete(url: String): Response {
    return executeAndSetResponse {
      StateHolder.getRequest().delete(url)
    }
  }
} 

Методы в HttpUtils считывают payload (если требуется) и используют Rest Assured Request из State (состояния) текущего потока для выполнения HTTP-вызова. После получения ответа он сохраняет результат в State response.

Вставьте приведенный ниже фрагмент, чтобы завершить ранее созданный createAnArticleWithFollowingFields() в ArticleStepDefs.

// 1
withPayload(payload) {
  HttpUtils.executePost("/${Resources.ARTICLES}")
}
// If successful, store the "id" field in differentiator for use in later steps
if (StateHolder.getResponse().statusCode == 200) {
  // 2  
  StateHolder.setDifferentiator(
      StateHolder.extractPathValueFromResponse("id")!!
  )
}

Здесь:

1. Вы сделали POST-запрос к конечной точке /articles, предоставив payload в виде тела.

2. Напомним, что response State хранит ответ, полученный в результате HTTP-запросов. Если запрос успешен, вы извлекаете значение поля “id” из ответа и сохраняете его в differentiator. Позже вы будете использовать его для получения статьи по ее идентификатору.

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

 

Expected vs. Actual (ожидаемые и фактические)

Хорошо! Вернемся к файлу create-article.feature. В ранее созданном сценарии добавьте следующие шаги:

Then Should succeed
And "id" should not be null
And "title" should not be null
And "body" should not be null
And "lastUpdatedOn" should not be null
And "createdOn" should not be null
And "title" should be equal to "Cucumber"
And "articleType" should be equal to "FREE"
And "title" should be same as that in payload

When Fetch article by id
Then Should succeed
And "title" should be equal to "Cucumber"
And "id" should be equal to differentiator

Значения параметров, такие как ID, title и body, являются именами полей в ответе. Затем создайте соответствующие определения шагов в ArticleStepDefs:

@Then("{string} should not be null")
fun shouldNotBeNull(path: String) {
  // 1
  StateHolder.getValidatableResponse().body(path, notNullValue())
}

@Then("{string} should be equal to {string}")
fun shouldBeEqual(path: String, right: String) {
  StateHolder.getValidatableResponse().body(path, equalTo(right))
}

@Then("{string} should be equal to differentiator")
fun shouldBeEqualToDifferentiator(path: String) {
  StateHolder.getValidatableResponse().body(
      path,
      equalTo(StateHolder.getDifferentiator())
  )
}

@Then("{string} should be same as that in payload")
fun pathValueShouldBeSameAsPayload(path: String) {
  val valueFromResponse = StateHolder.getValidatableResponse()
      .extract().body().path(path)
  val valueFromPayload = StateHolder.getPayloadAsMap()[path]
  assert(valueFromResponse.equals(valueFromPayload))
}

@When("Fetch article by id")
fun fetchArticleById() {
  // 2
  val id = StateHolder.getDifferentiator()
  requireNotNull(id)
  HttpUtils.executeGet("/${Resources.ARTICLES}/${id}")
}

@Then("Should succeed")
fun requestShouldSucceed() {
  assertThat(
      StateHolder.getResponse().statusCode,
      allOf(
          greaterThanOrEqualTo(200),
          lessThan(300)
      )
  )
}

@Then("Should have status of {int}")
fun requestShouldHaveStatusCodeOf(statusCode: Int) {
  assertThat(
      StateHolder.getResponse().statusCode,
      equalTo(statusCode)
  )
}

Здесь:
1. getValidatableResponse() предоставляет удобный способ проверки ответа. Большинство методов assertion похожи, потому что они считывают значение в response, указанном именем поля, и используют Matchers Hamcrest для его подтверждения.
2. Напомним, что createAnArticleWithFollowingFields() также сохраняет значение поля “id” в differentiator. Вы используете его для получения соответствующей статьи, которая сохраняется в ответ.

Найдите минутку, чтобы понять вышеперечисленные методы и их отношение к шагам с Gherkin.
Дополнительный импорт должен выглядеть так:
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.*
// ...

Наконец, запустите Java Cucumber конфигурацию Feature: create-article, чтобы выполнить сценарий. Вы должны увидеть что-то вроде этого:

Прекрасная работа! Вы завершили свой первый сценарий.

 

Feature: Get Article

Итак, вы научились писать шаги Gherkin и реализовывать их методы определения шагов. Теперь добавьте каталог get в /src/test/resources/com/raywenderlich/artikles/. Создайте там файл функции с именем get-article.feature и вставьте следующее:

Feature: Get Article.

  Scenario: As a user, I should be able to get all articles.
    Given Bulk create articles with following fields
      | title    | body                    |
      | Hamcrest | A testing library       |
      | AssertJ  | Fluent testing library  |
      | TDD      | Test Driven Development |
    When Fetch all articles
    Then Should succeed
    And Should have size of 3

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

Чтобы реализовать определения шагов, откройте ArticleStepDefs.kt и добавьте методы:

@Given("Bulk create articles with following fields")
fun bulkCreateArticles(payloads: List>) {
  // 1
  payloads.forEach {
    createAnArticleWithFollowingFields(it)
  }
}

@When("Fetch all articles")
fun fetchAllArticles() {
  // 2
  HttpUtils.executeGet(Resources.ARTICLES)
}

@Then("Should have size of {int}")
fun shouldHaveSizeOf(size: Int) {
  assertThat(
      StateHolder.getValidatableResponse().extract().body().path(""), // 3
      hasSize(size)
  )
}

Здесь:
1. Поскольку для массового создания не существует конечной точки, перебираете значения в таблице данных, вызывая createAnArticleWithFollowingFields() для каждой строки.
2. Отправляйте запрос GET request на конечную точку /articles. Ответом будет массив JSON.
3. Считываете корень, то есть весь массив JSON, как List (список). Затем проверяете его размер.

Теперь создайте копию Java Cucumber конфигурацию Feature: create-article. Назовите её Feature: get-article. Измените путь к функции на расположение файла для get-article.feature. Затем запустите, чтобы выполнить тесты.

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

Скопируйте файлы ArticleStepDefs.kt, get-article.feature, delete-article.feature и update-article.feature из финального проекта в свой начальный проект. В get-article.feature закомментируйте строку, содержащую @requiresDBClear, с помощью #. О тегах вы узнаете позже.

Один интересный сценарий в get-article.feature использует Scenario Outline для описания шаблона и Examples (примеры) для определения комбинации входных данных.

Этот сценарий выполняется трижды - один раз для каждого значения ID, заменяя "<id>" на каждом шаге.

Ваши тесты могут пройти или не пройти в зависимости от порядка выполнения файла функций. Причина:

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

Чтобы сделать это утверждение менее строгим, либо измените шаг на что-то вроде Should have size of at least 3 вместо Should have size of 3, либо очистите базу данных перед выполнением этого сценария. Далее вы узнаете, как организовать с помощью тегов.

 

Tags (тэги)

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

Раскомментируйте ранее закомментированный @requiresDBClear. Затем перейдите к ArticleStepDefs и взгляните на requiresDBClear:

@Before("@requiresDBClear")
fun requiresDBClear(scenario: Scenario) {
  println("Clearing table for ${scenario.name}")
  _repository.deleteAll()
}

requiresDBClear() содержит conditional hook (условный перехватчик), который запускается перед сценариями, помеченными тегом @requiresDBClear. Вы очищаете всю таблицу перед выполнением любого из шагов.

Запустите тесты. Теперь вы обнаружите, что все тесты пройдены.

Вы также можете комбинировать теги, используя @After("@tag1 and not @tag2") или @After("@tag1 and @tag2").

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

 

Параллельное выполнение тестов

Откройте build.gradle и передайте ConsoleLauncher следующие аргументы после строки args ("--scan-classpath"):

systemProperty("cucumber.execution.parallel.enabled", true)
systemProperty("cucumber.execution.parallel.config.strategy", "dynamic")

Выполните эту задачу с помощью ./gradlew test. Используйте ./gradlew test | grep \#\# для фильтрации логов. Вы можете видеть, что функции выполняются несколькими потоками.

Как и раньше, успех этих тестов также зависит от порядка выполнения. Если сценарий, помеченный тегом @requiresDBClear, выполняется параллельно с другим сценарием, в котором вы только что создали статью, и прямо перед шагом “fetch article by its id”, сценарий завершится неудачно, поскольку таблица уже будет очищена.

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

 

Запуск тестов изолированно

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

Создайте файл src/test/resources/junit-platform.properties и добавьте следующее свойство:

cucumber.execution.exclusive-resources.isolated.read-write=org.junit.platform.engine.support.hierarchical.ExclusiveResource.GLOBAL_KEY

Перейдите в файл get-article.feature и пометьте Feature с помощью @isolated.

Теперь функция get-article.feature выполняется изолированно от других функций.

Повторно запустите тесты, и вы увидите, что все тесты пройдены.

Вы можете прочитать об этом больше в документации cucumber-unit-platform-engine documentation.

Далее вы узнаете о плагинах для создания отчетов.

 

Плагины отчетов

Cucumber предоставляет reporting plugins, которые вы можете использовать для создания отчетов.

Чтобы настроить плагины отчетов, перейдите в build.gradle и укажите ConsoleLauncher следующий аргумент:

systemProperty(
    "cucumber.plugin",
    "pretty, summary, timeline:build/reports/timeline, html:build/reports/cucumber.html"
)

Вы предоставили четыре плагина в формате CSV, а именно: pretty, summary, timeline и html.

Summary выводит итоговый тест в конце.

Плагин html генерирует отчеты в формате HTML по адресу build/report/cucumber.html.

Плагин timeline создает отчет в build/report/timeline, который показывает, как и какой поток выполнял сценарии, что отлично подходит для отладки нестабильных тестов.

Помните тег @isolated? В приведенном выше отчете показано выполнение get-article.feature отдельно от других функций.

Вы прошли весь путь! Вы узнали, как интегрировать Cucumber в приложение Spring Boot, писать и реализовывать определения шагов, использовать несколько потоков для их параллельного выполнения и создавать отчеты с помощью плагинов.

У Cucumber есть хорошо написанная документация по его использованию, а также по анти-шаблонам и BDD.

Оригинал статьи

Содержание