How to Add Cucumber Layer on Top of REST-assured API Tests

This post provides a step-by-step guide on how to add a cucumber layer on top of api tests written in REST-assured.

REST-assured’s DSL already provides a BDD-style writing of tests in the Given-When-Then format, but it is still buried in the code. In other words, if you want to see what scenarios are covered, you still have to dig down into the api tests and read the code. There are no feature files.

The aim of this post is to refactor existing REST-assured api tests by adding cucumber and feature files, so that scenarios can be read more clearly without having to look at the underlying code.

REST-assured API Tests

In this example, we will write code to test user creation api.

First, we have a standalone REST-assured and JUnit Test, which resides in:

src/test/java/io.devqa/scenarios

import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import io.restassured.response.Response;
import org.junit.jupiter.api.*;
import static io.restassured.RestAssured.given;

public class UserTests {

    private static String path;

    private static String validRequest = "{\n" +
            "  \"username\": \"test-api-user\",\n" +
            "  \"email\": \"test-api-user@email.com\",\n" +
            "  \"password\": \"Passw0rd123!\",\n" +
            "  \"name\": \"Test Api-User\" \n}";

    @BeforeAll
    public static void setConfig() {
        RestAssured.baseURI = "https://localhost:8080";
        path = "/users";
    }

    @Test
    public void shouldBeAbleToCreateNewUser() {
        Response createUser = given()
                .auth()
                .preemptive()
                .basic("MY_USERNAME", "MY_PASSWORD")
                .header("Accept", ContentType.JSON.getAcceptHeader())
                .contentType(ContentType.JSON)
                .body(validRequest)
                .post(path)
                .then().extract().response();
        Assertions.assertEquals(201, createUser.getStatusCode());

        String username = createUser.jsonPath().get("username");
        String email = createUser.jsonPath().get("email");
        String name = createUser.jsonPath().get("name");
        String id = createUser.jsonPath().get("id");

        Assertions.assertEquals("test-api-user", username);
        Assertions.assertEquals("test-api-user@email.com", email);
        Assertions.assertEquals("Test Api-User", name);
        Assertions.assertNotNull(id);
    }
}

The above test can be run directly from the class as it can be invoked by JUnit.

The setConfig() method sets the pre-requisite. The test method does the actions (sending the request) and then asserting on the response code and response payload.

Next, we will look at how to put the cucumber layer on top of the above REST-assured api test.

Cucumber and REST-assured API Tests

The first thing we need to do is to add the cucumber dependency in our project.

Using Gradle, in our build.gradle file, we put these under the dependencies:

dependencies {
    testCompile "io.cucumber:cucumber-java:6.2.2"
    testCompile "io.cucumber:cucumber-junit:6.2.2"
    testCompile "io.rest-assured:rest-assured:3.3.0"
    testCompile "com.jayway.jsonpath:json-path:2.4.0"
}

And these under configuration in build.gradle file:

configurations {
    cucumberRuntime {
        extendsFrom testImplementation
    }
}

We also need to create a task in the build.gradle file to run the cucumber feature files which contain the scenarios:

task cucumber() {
    dependsOn assemble, compileTestJava
    doLast {
        mkdir 'build/test-results/'
        javaexec {
            main = "io.cucumber.core.cli.Main"
            classpath = configurations.cucumberRuntime + sourceSets.main.output + sourceSets.test.output
            args = ['--plugin', 'pretty', '--plugin', 'html:build/test-results/functional.html', '--plugin', 'junit:build/test-results/functional.xml','--tags', '@functional', '--glue', 'scenarios', 'src/test/resources']
        }
    }
}

Project Structure for Cucumber

We also need to modify our project structure to accommodate the changes for cucumber.

The feature files will be saved in:

src/test/resources/scenarios

The step definitions will be save in

src/test/java/scenarios

Next, we will create a feature file called UserScenarios.feature and put it under src/test/resources/scenarios folder.

The feature file will look like:

@functional
Feature: User Scenarios

  Scenario: I should be able to create a new user
    Given the users endpoint exists
    When I send a valid create user payload
    Then response status code should be 201
    And create user response should be valid

Now we need to dismantle our REST-assured JUnit test to write step definitions that can be glued to the statements in our feature file.

import io.cucumber.java.en.And;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;

import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import io.restassured.response.Response;

import org.junit.jupiter.api.Assertions;
import static io.restassured.RestAssured.given;

public class UserScenarios {

    private String path;
    private Response response;

    private String validRequest = "{\n" +
            "  \"username\": \"test-api-user\",\n" +
            "  \"email\": \"test-api-user@email.com\",\n" +
            "  \"password\": \"Passw0rd123!\",\n" +
            "  \"name\": \"Test Api-User\" \n}";


    @Given("the users endpoint exists")
    public void preReq() {
        RestAssured.baseURI = "https://localhost:8080";
        path = "/users";
    }

    @When("I send a valid create user payload")
    public void createUser() {
        response = given()
                .auth()
                .preemptive()
                .basic("MY_USERNAME", "MY_PASSWORD")
                .header("Accept", ContentType.JSON.getAcceptHeader())
                .contentType(ContentType.JSON)
                .body(validRequest)
                .post(path)
                .then().extract().response();
    }

    @Then("response status code should be {int}")
    public void checkResponseStatusCode(int code) {
        Assertions.assertEquals(code, response.getStatusCode());
    }

    @And("create user response should be valid")
    public void verifyResponse() {
        String username = response.jsonPath().get("username");
        String email = response.jsonPath().get("email");
        String name = response.jsonPath().get("name");
        String id = response.jsonPath().get("id");

        Assertions.assertEquals("test-api-user", username);
        Assertions.assertEquals("test-api-user@email.com", email);
        Assertions.assertEquals("Test Api-User", name);
        Assertions.assertNotNull(id);
    }
}

As can be seen in the above step definitions, for every line in the scenario in the feature file, we have a corresponding step definition.

The method with the Given annotation sets the pre-requisites. The method with the When annotation does the action of sending the request and finally the method with the Then annotation performs the assertions on the response.

To execute the above, all we need to do is to execute the command ./gradle cucumber in a terminal from the project root.

Once the tests have run, the results are saved in build/test-results/functional.html.

Conclusion

In this post, we covered a step-by-step guide on how to add a cucumber layer on top of the REST-assured API tests. By doing so, we can write our scenarios in feature files which become more readable by non-technical people.