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
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
.
{
"userId": 1,
"id": 1,
"title": "delectus aut autem",
"completed": false
}
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.