Press "Enter" to skip to content

A beginner’s guide to writing powerful BDD tests using Rest Assured and Cucumber

Vladimir Simonovski

Organizations today tend to invest a lot of effort and resources in setting an API test automation strategy. Having a well-structured test automation framework allows for a better test regression suite and better quality product, enhancing the test coverage and reducing the manual repetitive test efforts. Since automation testing plays an important role in software testing, companies can determine which tool can suit them the most to achieve the above-mentioned.

In this blog post, we are going to focus on working with the Rest Assured library for API testing as and Cucumber framework for BDD tests. We’re going to explain what are their purpose and will provide a simple example of how combining them together makes for powerful and well-structured tests.

What is Cucumber BDD and why it is used?

Cucumber is a framework for writing automated tests in a BDD fashion. Its syntax (Gherkin language) allows for more readable and comprehensive tests which can be understood by any member of the team. Cucumber can be combined with many API or UI frameworks, it’s easy to install and easy to use once you get a grip on it. If you want to know more about Cucumber in detail check out write automated tests in behavior-driven development fashion blog post.

What is Rest Assured and why is it used?

Rest Assured is a Java library that is used for writing automated API tests. It is one of the most popular tools you can find today. Basically, Rest Assured acts like an HTTP client where can create CRUD operations against some API Restful server. They can be highly customizable so that we can have the flexibility of performing many different combinations of test cases to cover the overall business logic. Many features can be utilized as far as testing with Rest Assured goes.

We can create the request to ping the specified API endpoint, we can assert the status code, header values as well as response body returned from the call. Rest Assured incorporates the hamcrest assertion library by default, so no need to install any additional assertion plugins or similar.

Let’s dive into writing a simple automated test using Rest Assured and Cucumber BDD.

Write our first test

Setup

For setting up the project test structure you will need a couple of things first. Let’s assume that you are using Maven build automation tool. Since all maven projects have a pom.xml file which is the core unit containing all the information about the project, external plugins, dependencies, and configurations that the project needs, we will need to add a couple of dependencies first before starting to write the test. As an example API, we will use the Dummy Rest API, specifically the GET all employees and create employee API endpoints.

Let’s create our maven based project called API-AUTOMATION:

Rest Assured

In the pom.xml we need to add these dependencies:

<dependency>
    <groupId>io.rest-assured</groupId>
    <artifactId>rest-assured</artifactId>
    <version>4.3.0</version>
    <scope>compile</scope>
</dependency>

<dependency>
    <groupId>io.cucumber</groupId>
    <artifactId>cucumber-java</artifactId>
    <version>5.2.0</version>
</dependency>

<dependency>
    <groupId>io.cucumber</groupId>
    <artifactId>cucumber-jvm-deps</artifactId>
    <version>1.0.6</version>
    <scope>provided</scope>
</dependency>
POJO’s

In the main folder, we have created the response and request models using Lombok builder feature and we also created some validation methods that can be used later for assertion purposes.

The response and request POJO’s created are:

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class GetEmployeeResponseModel {

private List<EmployeeData> data;
private String status;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PostEmployeeRequestModel {

private String name;
private String salary;
private String age;
}

The first field is named data which is a list of objects, specifically a list of employees info. We need to create a model for that also.

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class EmployeeData {

    private int id;
    private String employee_name;
    private int employee_salary;
    private int employee_age;
}

Since we created the POJOs for the response and request body, we will need them later for sending, receiving, and assertion purposes.

Request & Response Specification

This is the part where we need to create our request and assert everything we want to assert as a response to that request.

Request specification is an interface where we can specify how our request would look like. Everything that we need to create a request, for example, the URL, auth token, base path, etc. is included in the interface. An example of a request specification request would look like this:

RequestSpecification requestSpecification = given();

requestSpecification.baseUri("http://dummy.restapiexample.com");
requestSpecification.basePath("/api/v1/employees");
requestSpecification.contentType("application/json;charset=utf-8");
requestSpecification.header("Cache-Control", "no-cache");
requestSpecification.header("Connection", "keep-alive");

or to avoid calling request reference multiple times we can use the approach:

RequestSpecification requestSpecification = given()
.baseUri("http://dummy.restapiexample.com")
.basePath("/api/v1/employees")
.contentType("application/json;charset=utf-8")
.header("Cache-Control", "no-cache")
.header("Connection", "keep-alive");

From both examples, we can notice the keyword given(). This is the first step of creating the request and it servers as a background for passing URLs, header values, query parameters, cookies, etc.

After defining that, we just chain all of the other details to create the request, like the URL and path of the endpoint that we will call, the content-type which will be in JSON format as well as some important header details.

Response specification is also an interface but it is used for verifying the response from the request specification call. Let’s look at the examples below:

public static ResponseSpecification status200Ok() {
return new ResponseSpecBuilder()
.expectContentType("application/json;charset=utf-8")
.expectStatusCode(200)
.build();
}

public static ResponseSpecification status201Created() {
return new ResponseSpecBuilder()
.expectContentType("application/json")
.expectStatusCode(200)
.build();
}

We have two response specification methods, one for the GET and POST request. We defined them as public static since we want to call them directly on all test classes and not create an instance of the class and also, these can serve as helper methods for assertion purposes so once we create them, we will use them across the entire test repo.

By using ResponseSpecBuilder class on the same chaining principle we are building the response. And by “building” I mean we specify what we want to verify as part of one response. In the examples above we are verifying the content-type and the status code from the two calls.

Test folder structure

We have the request & response models and assertion helper methods in place. Now we need to start creating the test folder structure which following the Cucumber BDD approach.

Rest-Assured
Test folder structure
Feature

The main purpose of the Cucumber framework is to have understandable tests. And to achieve that we need our tests to be comprehensive enough that they can be easily understood by non-IT individuals in the team and within the organization. Feature files are files containing the test scenarios written in BDD format using the Gherkin language. More info on all of the keywords and their purpose can be found in this blog post.

Let’s create our two scenarios, one for getting all the employees and one for creating an employee:

Feature: Employees Card

Scenario: List all employees
Given There are employees
When I fetch all employees
Then The employees are listed

Scenario: Create an employee
Given The POST endpoint and the request payload
When I send a POST request for creating an employee
Then The employee is successfully created

Both scenarios are straightforward to comprehend. They both have preconditions, action, and an outcome that needs to be verified from the action.

Step Definitions

Step definition classes are the classes that contain all the API logic/manipulation on the endpoints under test and it is the place where we map every sentence from the feature file scenarios to a method in the step definition class. Let’s create two-step definition classes for both scenarios.

List all employees

public class ListEmployeeStepDefinition {

    RequestSpecification requestSpecification;
    GetEmployeeResponseModel getEmployeeResponse;

    @Given("There are employees")
    public void thereAreEmployees() {
        requestSpecification = given();
    }

    @When("I fetch all employees")
    public void iFetchTheEmployees() {
        getEmployeeResponse = requestSpecification.when().get(GET_EMPLOYEES) .then().assertThat().spec(status200Ok()).extract().response().as(GetEmployeeResponseModel.class);
    }

    @Then("The employees are listed")
    public void theEmployeesAreListed() {
        assertThat(getEmployeeResponse.getStatus(), is(equalTo("success")));
        assertThat(getEmployeeResponse.getData(), hasItem(allOf(
                hasProperty("id", is(equalTo(1))),
                hasProperty("employee_name", is(equalTo("Tiger Nixon"))),
                hasProperty("employee_salary", is(equalTo(320800))),
                hasProperty("employee_age", is(equalTo(61)))
        )));
    }
}

Create employee

public class CreateEmployeeStepDefinition {

private RequestSpecification requestSpecification;
private ValidatableResponse getEmployeeResponse;
private PostEmployeeRequestModel postEmployeeRequestModel;

@Given("The POST endpoint and the request payload")
public void thePostEndpointAndRequestPayload() {
requestSpecification = given();
postEmployeeRequestModel = PostEmployeeRequestModel.builder()
.age("55")
.salary("400000")
.name("Vlad")
.build();
}

@When("I send a POST request for creating an employee")
public void iSendAPostRequestToCreateEAEmployee() throws JsonProcessingException {
getEmployeeResponse = requestSpecification
.contentType(ContentType.JSON)
.body(objectToString(postEmployeeRequestModel))
.when().post(CREATE_EMPLOYEE)
.then().assertThat().spec(status201Created());
}

@Then("The employee is successfully created")
public void theEmployeeIsSuccessfullyCreated() {
getEmployeeResponse
.body("status", is(equalTo("success")))
.body("message", is(equalTo("Successfully! Record has been added.")))
.body("data", is(notNullValue()));
}
}

As you can see from the classes above, we have 3 steps for each class. In the first class in the Given method, we are setting the given() keyword as a precondition and in the second class, we create the request payload using the builder annotation that would be used to create an employee.

In the When step, there is the action part where we are calling the endpoints GET_EMPLOYEES and CREATE_EMPLOYEES (static variables) and assert that status code and content-type are matching.

.spec(status201Created()) & .spec(status200Ok())

In the GET_EMPLOYEES scenario we are extracting the response as GetEmployeeResponseModel object:

.as(GetEmployeeResponseModel.class)

This is useful if we want to perform any assertions on the extracted object. In this case, we’re extracting the object and storing it in the getEmployeeResponse object model.

In the Then step, we are performing the final assertions on the response body. Using the built-in Hamcrest matches we are asserting the various fields like getStatus(), getData() fetched from the response model we created earlier in the list employee, and in the create employee we just hard-code the keys and assert if they have the expected values.

Test Runner

Since we have the test setup and the tests, we just need to run them. In order to run them we a test runner. TO create a test runner we create a new class called TestRunner. It would look something like this:

@RunWith(Cucumber.class)
@CucumberOptions(
        features= {"src/test/java/RestAssuredCucumberTests/Features"},
        glue= {"RestAssuredCucumberTests/StepDefinitions"},
        monochrome = true,
        plugin = { "pretty" }
)
public class TestRunner {

}

The runner will be Cucumber based (@RunWith(Cucumber.class)) and in the options section we specify:

  • The path to the feature files
  • The glue, which is the step definitions classes path
  • Make the output more readable with monochrome= true and plugin = { “pretty” }

Now we have everything setup, and that means we can run the tests!

Test Execution Result

Let’s execute the tests:

Cucumber & Rest Assured test execution

Both tests are passing, all steps are shown as an output so you can easily see what steps are being executed one after another. Let’s change something in the expected result to intentionally fail one test. (add “11” after Tiger Nixon string). After the second execution, the response for the first test would be:

ava.lang.AssertionError: 
Expected: a collection containing (hasProperty("id", is <1>) and hasProperty("employee_name", is "Tiger Nixon11") and hasProperty("employee_salary", is <320800>) and hasProperty("employee_age", is <61>))
     but: mismatches were: [hasProperty("employee_name", is "Tiger Nixon11")  property 'employee_name' was "Tiger Nixon", hasProperty("id", is <1>)  property 'id' was <2>, hasProperty("id", is <1>)  property 'id' was <3>, hasProperty("id", is <1>)  property 'id' was <4>, hasProperty("id", is <1>)  property 'id' was <5>, hasProperty("id", is <1>)  property 'id' was <6>, hasProperty("id", is <1>)  property 'id' was <7>, hasProperty("id", is <1>)  property 'id' was <8>, hasProperty("id", is <1>)  property 'id' was <9>, hasProperty("id", is <1>)  property 'id' was <10>, hasProperty("id", is <1>)  property 'id' was <11>, hasProperty("id", is <1>)  property 'id' was <12>, hasProperty("id", is <1>)  property 'id' was <13>, hasProperty("id", is <1>)  property 'id' was <14>, hasProperty("id", is <1>)  property 'id' was <15>, hasProperty("id", is <1>)  property 'id' was <16>, hasProperty("id", is <1>)  property 'id' was <17>, hasProperty("id", is <1>)  property 'id' was <18>, hasProperty("id", is <1>)  property 'id' was <19>, hasProperty("id", is <1>)  property 'id' was <20>, hasProperty("id", is <1>)  property 'id' was <21>, hasProperty("id", is <1>)  property 'id' was <22>, hasProperty("id", is <1>)  property 'id' was <23>, hasProperty("id", is <1>)  property 'id' was <24>]

A classic Hamcrest assertion validation response saying that the string Tiger Nixon is not equal to Tiger Nixon11.

So, that is all for this example folks! If you have any questions regarding this topic, let me know by contacting me at [email protected]. Thanks!

Share This Post


Latest Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.