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
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

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.
{
  "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?

  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.