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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Getting Started with Cucumber
30 mins
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.
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:
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
orExample
to group a series of steps. -
Given
,When
,Then
,And
andBut
to describe steps. These steps are executed sequentially. -
Scenario Outline
to run the sameScenario
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?
- 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.
- This adds Cucumber’s core classes and annotations to the test scope.
- cucumber-spring is required because the application needs to start and be ready to accept HTTP requests before executing any of the steps.
- cucumber-junit-platform-engine is the engine JUnit Console Launcher uses to execute Cucumber’s scenario.
- 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.
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:
- You provided path parameters and headers and created
RequestSpecification
using them. This is the “given” section. - Using the
RequestSpecification
created above, you sent a GET request tohttps://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 }
- Once you receive the HTTP response, validate it against your expectations. The
then()
returns aValidatableResponse
, which provides a fluent interface where you can chain your assertion methods. - 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?
- In the current configuration, Spring Boot starts listening for HTTP requests on a random port.
@LocalServerPort
binds this randomly allocated port to theport
. -
@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. - 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.
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.
Press the Run button. You should see the test passing.
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:
-
getRequest()
returns a minimally configuredRequestSpecification
if the current thread’sState
has no associated instance ofRequestSpecification
. Recall the “Given” section of Rest Assured’s Given-When-Then. -
setPayload()
sets the HTTP body ofRequestSpecification
returned bygetRequest()
. This is useful for PUT and POST requests. -
clear()
removes theState
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:
- Converts HTTP payload to another class.
- Exposes APIs to assert Rest Assured’s
Response
against expectations. - 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:
- You made a POST request to
/articles
endpoint, supplyingpayload
as a body. - Recall that the
response
ofState
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 atdifferentiator
. 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:
-
getValidatableResponse()
provides a convenient way to validate a response. Most of the assertion methods are similar because they read the value in theresponse
pointed by the field name and use Hamcrest’sMatchers
to assert it. - Recall that
createAnArticleWithFollowingFields()
also stores the value of the “id” field indifferentiator
. You use it to fetch the corresponding article, which gets stored inresponse
.
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:
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:
- Because no endpoint exists for bulk creation, iterate over the values in the data table, calling
createAnArticleWithFollowingFields()
on each row. - Send a GET request to
/articles
endpoint. The response will be a JSON array. - 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.
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:
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.
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:
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.
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")
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.
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
.
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.
The plugin html generates HTML reports at build/report/cucumber.html.
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.
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.
All videos. All books.
One low price.
A Kodeco subscription is the best way to learn and master mobile development — plans start at just $19.99/month! Learn iOS, Swift, Android, Kotlin, Flutter and Dart development and unlock our massive catalog of 50+ books and 4,000+ videos.
Learn more