Move REST API Calls to Step Definitions Base Class

Calling REST APIs to create a resource, update a resource, get a resource, delete a resource require some boiler plate code. In our example, we are creating API for a HR Software. In order to test the API we need to call them from step definition classes. If we can move the code to call REST APIs to base class, every step definition class that needs to call REST API can leverage that instead of again and again writing code to call APIs. Here, let us see how and what can be moved to step definition base class.


Following method calls POST /v1/employees REST API to create an employee.

...
@When("user saves a new employee(.*)")
public void userSavesANewEmployee() {
  String uri = baseUrl() + "/v1/employees";

  response = given().log()
      .all()
      .body(employee)
      .contentType(ContentType.JSON)
      .post(uri);

  response.then()
      .log()
      .all();
}
...

In the above code, we use RestAssured static builder methods given(), log(), post() etc to call the REST API and log the request and response. Similarly, we need to call HTTP GET, PUT, DELETE, PATCH methods for multiple APIs. Instead of repeating this 7 - 8 lines of code in each method, we can abstract all HTTP calls to a base class use it in all step definitions class by extending the abstract base class.

Step 1: Create Abstract Step Definitions Class

Navigate to following location and create the abstract step definitions class,

cd src/test/java/com/madrascoder/cucumberbooksample/stepdefinitions
touch CommonStepDefinitions.java

Add the following code,

Below class contains methods to call REST APIs, log request, log response. It reads the state from the auto wired TestContext.

import com.madrascoder.cucumberbooksample.TestContext;
import io.restassured.http.ContentType;
import io.restassured.response.Response;
import io.restassured.specification.RequestSpecification;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.server.LocalServerPort;

public abstract class AbstractStepDefinitions {

  // By default, all step definitions are Spring Beans
  // You may autowire Spring Beans and Properties in Cucumber StepDefinition class
  @LocalServerPort
  private int port;

  @Autowired
  private TestContext testContext;

  protected String baseUrl() {
    return "http://localhost:" + port;
  }

  protected TestContext testContext() {
    return testContext;
  }

  protected void executePost(String apiPath) {
    executePost(apiPath, null, null);
  }

  protected void executePost(String apiPath, Map<String, String> pathParams) {
    executePost(apiPath, pathParams, null);
  }

  protected void executePost(String apiPath, Map<String, String> pathParams, Map<String, String> queryParams) {
    final RequestSpecification request = testContext.getRequest();
    final Object payload = testContext.getPayload();

    setPayload(request, payload);
    setQueryParams(pathParams, request);
    setPathParams(queryParams, request);

    Response response = request.contentType(ContentType.JSON)
        .accept(ContentType.JSON)
        .post(apiPath);

    logResponse(response);

    testContext.setResponse(response);
  }

  protected void executeMultiPartPost(String apiPath) {
    final RequestSpecification request = testContext.getRequest();
    final Object payload = testContext.getPayload();

    Response response = request.multiPart("fileName", payload, "application/json")
        .post(apiPath);

    logResponse(response);
    testContext.setResponse(response);
  }

  protected void executeDelete(String apiPath) {
    executeDelete(apiPath, null, null);
  }

  protected void executeDelete(String apiPath, Map<String, String> pathParams) {
    executeDelete(apiPath, pathParams, null);
  }

  protected void executeDelete(String apiPath, Map<String, String> pathParams, Map<String, String> queryParams) {
    final RequestSpecification request = testContext.getRequest();
    final Object payload = testContext.getPayload();

    setPayload(request, payload);
    setQueryParams(pathParams, request);
    setPathParams(queryParams, request);

    Response response = request.accept(ContentType.JSON)
        .delete(apiPath);

    logResponse(response);
    testContext.setResponse(response);
  }

  protected void executePut(String apiPath) {
    executePut(apiPath, null, null);
  }

  protected void executePut(String apiPath, Map<String, String> pathParams) {
    executePut(apiPath, pathParams, null);
  }

  protected void executePut(String apiPath, Map<String, String> pathParams, Map<String, String> queryParams) {
    final RequestSpecification request = testContext.getRequest();
    final Object payload = testContext.getPayload();

    setPayload(request, payload);
    setQueryParams(pathParams, request);
    setPathParams(queryParams, request);

    Response response = request.contentType(ContentType.JSON)
        .accept(ContentType.JSON)
        .put(apiPath);
    logResponse(response);
    testContext.setResponse(response);
  }

  protected void executePatch(String apiPath) {
    executePatch(apiPath, null, null);
  }

  protected void executePatch(String apiPath, Map<String, String> pathParams) {
    executePatch(apiPath, pathParams, null);
  }

  protected void executePatch(String apiPath, Map<String, String> pathParams, Map<String, String> queryParams) {
    final RequestSpecification request = testContext.getRequest();
    final Object payload = testContext.getPayload();

    setPayload(request, payload);
    setQueryParams(queryParams, request);
    setPathParams(pathParams, request);

    Response response = request.accept(ContentType.JSON)
        .patch(apiPath);

    logResponse(response);
    testContext.setResponse(response);
  }

  protected void executeGet(String apiPath) {
    executeGet(apiPath, null, null);
  }

  protected void executeGet(String apiPath, Map<String, String> pathParams) {
    executeGet(apiPath, pathParams, null);
  }

  protected void executeGet(String apiPath, Map<String, String> pathParams, Map<String, String> queryParams) {
    final RequestSpecification request = testContext.getRequest();

    setQueryParams(queryParams, request);
    setPathParams(pathParams, request);
    Response response = request.accept(ContentType.JSON)
        .get(apiPath);

    logResponse(response);
    testContext.setResponse(response);
  }

  private void logResponse(Response response) {
    response.then()
        .log()
        .all();
  }

  private void setPathParams(Map<String, String> pathParams, RequestSpecification request) {
    if (null != pathParams) {
      request.pathParams(pathParams);
    }
  }

  private void setQueryParams(Map<String, String> queryParams, RequestSpecification request) {
    if (null != queryParams) {
      request.queryParams(queryParams);
    }
  }

  private void setPayload(RequestSpecification request, Object payload) {
    if (null != payload) {
      request.contentType(ContentType.JSON)
          .body(payload);
    }
  }
}

If for some reason, you don’t like RestAssured and would like to replace it with another REST client library, you need to add the respective dependency and modify the REST client code only in this class. This is another benefit of abstracting REST API Call logic to an abstract class.

Step 2: Extend Abstract Step Definitions class in Employee Step Definitions class

Because many of the boiler plate REST API calls are moved to AbstractStepDefinitions.java, EmployeeStepDefinitions.java now became super thin with just one or two lines of code in each step definition methods.

import com.madrascoder.cucumberbooksample.dto.Employee;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.When;

public class EmployeeStepDefinitions extends AbstractStepDefinitions {

  @Given("user wants to create/update employee with following details")
  public void userWantsToCreateEmployeeWithFollowingDetails(Employee employee) {
    testContext().setPayload(employee);
  }

  // Background Step. It gets executed once for every Scenario or Example.
  @Given("a employee with following details already exists")
  public void aEmployeeWithFollowingDetailsAlreadyExists(Employee employee) {
    testContext().setPayload(employee);
    executePost(employeeResourceUrl());
  }

  @When("user saves a new employee(.*)")
  public void userSavesANewEmployee() {
    executePost(employeeResourceUrl());
  }

  @When("user saves employee")
  public void userSavesEmployee() {
    executePut(employeeResourceUrl());
  }

  private String employeeResourceUrl() {
    return baseUrl() + "/v1/employees";
  }
}

Compared with the one we had in previous chapter, you know how simple it is now.


Conclusion

In this chapter, we moved all boiler plate REST API call code to AbstractStepDefinitions.java class. This simplified EmployeeStepDefinitions.java class and it will make any other new step definitions class also a simple one.

In the next chapter, we will learn how to deal with JPA/Hibernate auto generated identifiers from testing perspective.


Credits

Photo by Mark Potterton on Unsplash


Previous Chapter | Scroll Up to Top | Table of Contents | Next Chapter