Getting Started with Cucumber

Learn to use Cucumber, Gherkin, Hamcrest and Rest Assured to integrate Behavior-Driven Development (BDD) in an application made using Spring Boot and Kotlin. By Prashant Barahi.

Leave a rating/review
Download materials
Save for later
Share

The Software Development Life Cycle (SDLC) generally involves two kinds of people: business professionals and engineers. Because their expertise is in different domains, the business requirements can easily be misunderstood or expressed unclearly and the final product might fail to meet the business needs. Behavior-Driven Development (BDD) is a software development process that encourages:

  • Collaboration among the whole team.
  • Use of ubiquitous language to define the behavior of the system, i.e. a specification.
  • Use of automated tests to validate the system against the specification.

The progression and the behavior of the system remain transparent across the team throughout the project’s lifecycle, ensuring a shared understanding of the problem to solve.

So a specification written in plain text that can be picked up by a testing framework and then executed? How do you make something like JUnit do that? Enter Cucumber! Cucumber reads specifications and validates what the software does against what the specification states. It helps you integrate BDD into your SDLC. Cucumber is available for many programming languages and the specifications can be written in various spoken languages.

In this tutorial, you’ll:

  • Use Gherkin to define the specifications.
  • Integrate Cucumber in a Spring Boot application.
  • Use assertion libraries like Hamcrest and Rest Assured to validate specifications.
  • Learn how to share state between steps.
  • Use multiple threads to execute tests in parallel.
  • Learn how to deal with flaky tests.
  • Generate test reports.

You’ll work with an application called artikles that’s made with Spring Boot and Kotlin.

Note: This article assumes you’re familiar with the basics of Spring Boot and Hibernate.

Getting Started

Click Download Materials at the top or bottom of the tutorial to download the starter project. Fire up the IntelliJ IDEA and select Open…. Then, navigate to and open the starter project.

Build and run the app. You’ll find the server running on port 8080.

Now, create the first article:

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

And send a GET request:

curl http://localhost:8080/articles
	

You’ll get the article you just created as a response. Great! Now that your application is up and running, fire up your browser and go to localhost:8080/h2-console. Log in using the following configurations:

H2 Console Login screen with the configuration values from application.properties

You can find these configurations in application.properties, located at src/main/resources. This application uses an in-memory H2 database. Keep in mind that stopping and restarting the application will reset the database. The starter project doesn’t contain any test cases yet. In the upcoming sections, you’ll learn how to integrate tests in this project. Before that, Gherkin!

Gherkin

The executable specification isn’t really “plain” text. It’s written in a structured format called Gherkin. Gherkin provides a set of grammar rules and keywords to describe the specification. The specifications live in *.feature files and contain one or more scenarios. Each scenario contains a series of steps Cucumber executes and validates against the business expectations.

A typical specification looks like this:

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 provides:

  • Feature to group related scenarios.
  • Scenario or Example to group a series of steps.
  • Given, When, Then, And and But to describe steps. These steps are executed sequentially.
  • Scenario Outline to run the same Scenario multiple times using different combinations of inputs.

You can provide a list of inputs to a step definition using Data Tables:

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

Refer to the Gherkin documentation to learn about the keywords and their usage. Next, you’ll learn about Cucumber.

Cucumber

Cucumber connects Gherkin steps to a step definition. A step definition is a method that’s annotated with one of the step keywords: (@Given, @When, @Then or @But). It contains either a Regular Expression or a Cucumber Expression that links the method to the Gherkin steps.

A step definition for the scenario above could look like this:

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)
  }

  // ...
}
	

The step definition methods either initialize or set up a state or validate the current state of the system.

Cucumber Expressions support basic parameter types like {int}, {float}, {string}, {biginteger}, {double}, {long} and {word}. You can learn more about them in the Cucumber Expressions documentation.

Now that you know about Cucumber, you’ll learn to integrate it in the Spring Boot project.

Setting up Cucumber

First, add the following dependencies to 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")
	

So, what’s going on here?

  1. Cucumber is un-opinionated toward the assertion library, so you can use which one you prefer. You’ll use Rest Assured to simulate the HTTP endpoints and Hamcrest to assert the HTTP response.
  2. This adds Cucumber’s core classes and annotations to the test scope.
  3. cucumber-spring is required because the application needs to start and be ready to accept HTTP requests before executing any of the steps.
  4. cucumber-junit-platform-engine is the engine JUnit Console Launcher uses to execute Cucumber’s scenario.
  5. Use the Console Launcher to launch the JUnit Platform and execute test cases from the console.

In build.gradle, replace the tasks.withType block with the following:

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("**/*")
  }
}
	

This configured the JUnit Console Launcher with the cucumber-junit-platform-engine to execute Cucumber’s scenarios.

Perform Gradle sync and execute that task using ./gradlew test on Unix machines or gradlew.bat test on Windows. You should see a gradle task run and then a block of text that looks something like this:

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          ]

Last, go to Preferences > Plugins and check that you installed the necessary plugins for Cucumber and enabled them in IntelliJ. You may have to restart IntelliJ after installing the plugins. These plugins help to make the testing flow much easier when using Cucumber.

Enable the IDE plugins for Gherkin, Cucumber for Groovy and Cucumber for Java

You’re now ready to integrate Cucumber in the Spring Boot. Before that, take a brief tour of Rest Assured.

Rest Assured

Rest Assured greatly simplifies the testing of REST services. As mentioned earlier, you’ll use it to simulate the REST controllers and then validate the response.

Its APIs follow a Given-When-Then structure. A simple test using Rest Assured looks like:

// 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())
//...
	

Here:

  1. You provided path parameters and headers and created RequestSpecification using them. This is the “given” section.
  2. Using the RequestSpecification created above, you sent a GET request to https://jsonplaceholder.typicode.com/todos/1?id=1. If successful, it returns a response with following schema:
    {
      "userId": 1,
      "id": 1,
      "title": "delectus aut autem",
      "completed": false
    }
    	
  3. Once you receive the HTTP response, validate it against your expectations. The then() returns a ValidatableResponse, which provides a fluent interface where you can chain your assertion methods.
  4. You asserted whether the status code and the HTTP response are what you expect using Hamcrest’s Matchers.

Rest Assured can do a lot more. Refer to the Rest Assured documentation to learn more. Next, you’ll see it in action.

Cucumber and Spring Boot

Spring Boot requires a few seconds before it can start serving the HTTP requests. So you must provide an application context for Cucumber to use.

Create a class SpringContextConfiguration.kt at src/test/kotlin/com/raywenderlich/artikles and paste the snippet below:

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

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

Here, you configured Spring Boot to run at a random port before the test execution starts.

At the same location, create a class ArticleStepDefs.kt that will hold all the step definitions and extend it from the class above.

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()
  }
}
	

So, what’s going on here?

  1. In the current configuration, Spring Boot starts listening for HTTP requests on a random port. @LocalServerPort binds this randomly allocated port to the port.
  2. @Before is a scenario hook that Cucumber calls once before every scenario. Although the @After scenario hook runs after each scenario. Similarly, @BeforeStep and @AfterStep are step hooks that run before and after each steps, respectively. You can use these to initialize or cleanup resources or state.
  3. Build application URI using the dynamically allocated port and use it to configure the base URL of Rest Assured.

Now, you’re ready to take your first step. :]

Feature: Create Article

Add a new directory namedcreate to /src/test/resources/com/raywenderlich/artikles/. Then, create a new file named create-article.feature there and paste the following snippet:

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 |
	

You defined a scenario in Gherkin. It contains only one step. You’ll add more steps later. You’ll use provided fields (as an HTTP payload) to create an article (send a POST request).

With your caret at the step, open the contextual menu. Choose Create step definition. Then, select the file where you will create the corresponding step definition method. In this case, it’s ArticleStepDefs.kt.

Contextual menu in IntelliJ that shows the option to create the step definition

You can also manually create a method inside ArticleStepDefs. Ensure the Gherkin step matches the Cucumber expression. For “Create an article with following fields”, the step definition method is:

@Given("Create an article with following fields")
fun createAnArticleWithFollowingFields(payload: Map<String, Any>) {
}
	

The method above receives the content of the data table in payload. You can send it as an HTTP payload of the POST request at /articles.

Finally, run the test using the CLI or an IDE. In IntelliJ create a new Cucumber Java configuration with the following values:

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

Select the feature file to execute in the Feature or folder path. The configuration will look like the image below.

Cucumber Java configuration for Feature: create-article

Press the Run button. You should see the test passing.

All tests completed successfully for the Feature: create-article configuration

Next, you’ll learn how to handle a common pain point in Cucumber — sharing state between steps.

State

A single step is handled by one of the many step definitions. You can link a single step definition method to unrelated steps, like to validate an assertion. You need to maintain a state that’s visible throughout the step definition methods while preventing the state from leaking into other scenarios and without making the steps tightly coupled or hard to reuse.

Because you’re testing from the viewpoint of a user or HTTP client, build the state around HTTP constructs such as request, response, payload and path variable (differentiator).

Create StateHolder.kt at src/test/kotlin/com/raywenderlich/artikles. Paste the snippet below:

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<State> = ThreadLocal.withInitial { State() }

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

store is an instance of ThreadLocal. This class maintains a map between the current thread and the instance of State. Calling get() on store returns an instance of State that’s associated with the current thread. This is important when executing tests across multiple threads. You’ll learn more about this later.

You need to expose the attributes of State. Paste the following methods inside 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()
}
	

These getter methods follow Kotlin’s getX() and getXOrNull() conventions. The atypical ones to note are:

  1. getRequest() returns a minimally configured RequestSpecification if the current thread’s State has no associated instance of RequestSpecification. Recall the “Given” section of Rest Assured’s Given-When-Then.
  2. setPayload() sets the HTTP body of RequestSpecification returned by getRequest(). This is useful for PUT and POST requests.
  3. clear() removes the State instance associated with the current thread.

The imports look like:

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
	

In the same class, add these helper methods:

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

// 1
fun getPayloadAsMap(): Map<*, *> {
  return getPayloadAs(Map::class.java)
}

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

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

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

The method:

  1. Converts HTTP payload to another class.
  2. Exposes APIs to assert Rest Assured’s Response against expectations.
  3. Extracts value from the HTTP response that’s mapped to the path.

Take a moment to explore this class. Next, you’ll see these methods in action.

Creating an HTTP Client

To simulate the REST controller, perform HTTP calls to /articles endpoint. Use Rest Assured for this.

Create HttpUtils.kt at src/test/kotlin/com/raywenderlich/artikles and paste the snippet below:

import io.restassured.response.Response

fun <T : Any> 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)
    }
  }
}
	

The methods in HttpUtils read payload (if required) and use Rest Assured’s Request from State instance of the current thread to make an HTTP call. After receiving the response back, it stores the result in State‘s response.

Paste the snippet below to complete the previously created createAnArticleWithFollowingFields() in 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<String>("id")!!
  )
}
	

Here:

  1. You made a POST request to /articles endpoint, supplying payload as a body.
  2. Recall that the response of State stores the response retrieved as a result of HTTP requests. If the request is successful, you extract the value of the “id” field from the response and store it at differentiator. Later, you’ll use it to fetch the article using its ID.

You haven’t asserted anything yet. In the upcoming sections, you’ll see how to validate the actual state against the expected.

Expected vs. Actual

Alright! Back to the create-article.feature file. In the previously created scenario, add the following steps:

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
	

The parameter values like ID, title and body are the field names in the response. Next, create corresponding step definitions in 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<Comparable<Any>>(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)
  )
}
	

Here:

  1. getValidatableResponse() provides a convenient way to validate a response. Most of the assertion methods are similar because they read the value in the response pointed by the field name and use Hamcrest’s Matchers to assert it.
  2. Recall that createAnArticleWithFollowingFields() also stores the value of the “id” field in differentiator. You use it to fetch the corresponding article, which gets stored in response.

Take a moment to understand the above methods and how they’re related to the Gherkin steps.

The additional imports should look like this:

import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.*
// ...
	

Finally, run the Cucumber Java configuration Feature: create-article to execute the scenario. You should see something like this:

All tests for the scenario completed successfully for the Feature: create-article configuration

Great job! You completed your first scenario.

Feature: Get Article

So far, you’ve learned how to write Gherkin steps and implement their step definition methods. Now, add a get directory to /src/test/resources/com/raywenderlich/artikles/. Create a feature file there named get-article.feature and paste the following:

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
	

This scenario validates if fetching all articles actually returns every article in the database. First, it bulk-creates three articles and then asserts the result has a length of three.

To implement the step definitions, open ArticleStepDefs.kt and add the methods:

@Given("Bulk create articles with following fields")
fun bulkCreateArticles(payloads: List<Map<String, Any>>) {
  // 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<List<Any>>(""), // 3
      hasSize(size)
  )
}
	

Here:

  1. Because no endpoint exists for bulk creation, iterate over the values in the data table, calling createAnArticleWithFollowingFields() on each row.
  2. Send a GET request to /articles endpoint. The response will be a JSON array.
  3. Read the root, i.e. the entire JSON array, as a List. Then, validate its size.

Now, create a copy of the Cucumber Java configuration Feature: create-article. Name this one Feature: get-article. Change the feature path to the file location for get-article.feature. Then, run to execute the tests.

All tests completed successfully for the Feature: get-article configuration

Open the final project for scenarios to update, delete and fetch a single article and the new step definitions. The test directory’s final structure will look like this:

Final project's Test directory structure that includes feature files to delete and update articles

Copy the final project’s ArticleStepDefs.kt, get-article.feature, delete-articles.feature and update-article.feature files to your Starter project. In get-articles.feature, comment out the line containing @requiresDBClear with #. You’ll learn about tags later.

One interesting scenario in get-article.feature uses Scenario Outline to describe a template and Examples to define a combination of inputs.

Scenario outline that includes four examples to use for the tests

This scenario gets executed three times — once for each value of ID, replacing "<id>" in each step.

Your tests could pass or fail depending on the order of execution of the feature file. The cause:

Scenario to bulk create articles and fetch them

Because you’re running these tests against a single Spring Boot application with a single database, the test will pass only if the above scenario processes before other scenarios that create articles. The scenario creates three articles, fetches all articles and asserts the length of the list is exactly three. That’s correct if this scenario was executed alone and against an empty database. This isn’t always achievable.

To make that assertion less strict, either change the step to something like Should have size of at least 3 instead of Should have size of 3 or clear the database before executing this scenario. Next, you’ll learn how to organize using tags.

Tags

Tags provide a way to organize your features and scenarios. They can refer to a subset of scenarios. With this, you can restrict execution to them or restrict the hooks. You can tag a scenario or feature, using @ followed by the name of the tag.

Uncomment the previously commented @requiresDBClear. Then, navigate to ArticleStepDefs and take a look at requiresDBClear:

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

requiresDBClear() contains a conditional hook that runs before those scenarios that are tagged with @requiresDBClear. You clear the entire table before any of the steps get executed.

Run the tests. Now, you’ll find all the tests are passing.

All tests completed successfully for the Feature: get-article configuration

You can also combine tags using @After("@tag1 and not @tag2") or @After("@tag1 and @tag2").

By default, Cucumber tests are executed using a single thread. A large number of tests can take a lot of time when executed in this manner. Next, you’ll see how you can execute them in parallel.

Running Tests in Parallel

Open build.gradle and provide the following arguments to ConsoleLauncher after the args("--scan-classpath") line:

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

build.gradle file with the new systemProperty values

Execute this task using ./gradlew test. Use ./gradlew test | grep \#\# to filter out the logs. You can see the features are executed by multiple threads.

Grep logs that list all of the scenarios

Like before, the success of these tests also depends on the order of execution. If the scenario tagged with @requiresDBClear gets executed in parallel with another scenario where you just created an article and right before the “fetch article by its id” step, then the scenario will fail because the table would already be cleared.

Run the tests in isolation to prevent this. You’ll learn that in the next section.

Running Tests in Isolation

To avoid flaky tests when multiple scenarios manipulate the same resource, you can synchronize the tests on that resource or simply run those tests in isolation.

Create a src/test/resources/junit-platform.properties file and add the following property:

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

Go to the get-articles.feature file and tag Feature with @isolated.

The get-articles.feature file with @isolated on the first line

Now, the get-article.feature feature gets executed in isolation from the other features.

Re-run the tests and you should see all the tests passing.

You can read more about this in the cucumber-unit-platform-engine documentation.

Next, you’ll learn about reporting plugins.

Reporting Plugins

Cucumber provides reporting plugins you can use to generate test reports.

To configure reporting plugins, go to build.gradle and provide the following argument to ConsoleLauncher:

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

You provided four plugins in a CSV format, namely pretty, summary, timeline and html.

The summary outputs a summary of the test at the end.

Reporting format for Cucumber's Summary plugin

The plugin html generates HTML reports at build/report/cucumber.html.

Reporting format for Cucumber's HTML plugin

The timeline plugin generates a report at build/report/timeline that shows how and which thread executed the scenarios, which is great for debugging flaky tests.

Reporting format for Cucumber's Timeline plugin

Remember the @isolated tag? The report above shows get-article.feature executed separately from the other features.

Where to Go From Here?

Click Download Materials at the top or bottom of the tutorial to download the final project.

You made it all the way through! You learned how to integrate Cucumber in a Spring Boot application, write and implement step definitions, use multiple threads to execute them in parallel and generate reports using plugins.

Cucumber has well-written documentation on its usage as well as anti-patterns and BDD.

It’s a good practice to automate the verification process and perform it frequently, preferably on every new change. Check out Continuous Integration for Android to get the gist of how to achieve it.

If you have any questions or comments, please join the forum discussion below.