Use BDD to Implement and Test 'GET APIs'

Previously, we learnt how to implement and test 'Create API', 'Update API'. Here, let us learn how to use BDD to implement and test 'GET APIs'. Also, let us see how we can compare the expected results with the actual results using AssertJ library.


Let’s create feature file, step definitions and an API for the following requirement/user story.

As a HR Staff,
I want to get employee details by id,
So that I view the details of the respective employee.

As a HR Staff,
I want to get employee details by last name,
So that I view details of all the employees who has the given last name.


Step 1: Create Feature File for ‘Get APIs’

Here is the feature file containing following scenarios

  1. Get employee by id
  2. Get employees by last name

1.1 Feature File

Navigate to following location and create a feature file,

cd src/test/resources/com/madrascoder/cucumberbooksample
touch 1110-get-employee.feature

Add the following feature,

Feature: Get Employee

  Background: Employee already exists

    Given a employee with following details already exists
      | id     | firstName | lastName | email              | dateOfBirth        | remoteWorker | jobTitle                     | employeeNumber | employeeStatus | employmentType |
      | 111001 | Tyrone    | Green    | tobeney0@hc360.com | LocalDate.now-6546 | NO           | Budget/Accounting Analyst II | 175            | Active         | Part-Time      |

    And a employee with following details already exists
      | id     | firstName | lastName  | email                   | dateOfBirth        | remoteWorker | jobTitle                      | employeeNumber | employeeStatus | employmentType |
      | 111002 | Shel      | Hendricks | shendricks1@walmart.com | LocalDate.now-6547 | YES          | Community Outreach Specialist | 183            | Inactive       | Contractor     |

    And a employee with following details already exists
      | id     | firstName | lastName | email                | dateOfBirth        | remoteWorker | jobTitle           | employeeNumber | employeeStatus | employmentType |
      | 111003 | Salli     | Green    | sduffitt2@rambler.ru | LocalDate.now-6548 | YES          | Biostatistician IV | 146            | Inactive       | Part-Time      |


  Scenario: Get employee by id

    When user wants to get employee by id 111002

    Then the get 'IS SUCCESSFUL'

    And following employee is returned
      | id     | firstName | lastName  | email                   | dateOfBirth        | remoteWorker | jobTitle                      | employeeNumber | employeeStatus | employmentType |
      | 111002 | Shel      | Hendricks | shendricks1@walmart.com | LocalDate.now-6547 | YES          | Community Outreach Specialist | 183            | Inactive       | Contractor     |


  Scenario: Get employee by last name

    When user wants to get employee by last name containing 'Gre'

    Then the get 'IS SUCCESSFUL'

    And following employees are returned
      | id     | firstName | lastName | email                | dateOfBirth        | remoteWorker | jobTitle                     | employeeNumber | employeeStatus | employmentType |
      | 111003 | Salli     | Green    | sduffitt2@rambler.ru | LocalDate.now-6548 | YES          | Biostatistician IV           | 146            | Inactive       | Part-Time      |
      | 111001 | Tyrone    | Green    | tobeney0@hc360.com   | LocalDate.now-6546 | NO           | Budget/Accounting Analyst II | 175            | Active         | Part-Time      |

In the feature Background, we setup 3 employees with ids (111001, 111002, 111003). Also, employee with id = ‘111001’ and ‘111003’ is set to have the same last name ‘Green’. This is to test ‘Get employees by last name’ use case.

If you look at the both the Scenarios, there is no Given Step. Yes, you can create a Scenario without a Given Step.

1.2 Corresponding Step Definitions

Look at the methods stated below, these are the new methods added to support testing ‘GET APIs’.

userWantsToGetEmployeeById(Integer id)

followingEmployeeIsReturned(Employee expectedEmployee) 

userWantsToGetEmployeeByLastNameContaining(String lastNameContaining)

followingEmployeesAreReturned(List<Employee> expectedEmployees) 

In the step definition class stated below,

import static org.assertj.core.api.Assertions.assertThat;

import com.madrascoder.cucumberbooksample.dto.Employee;
import io.cucumber.java.en.And;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.When;
import io.restassured.mapper.TypeRef;
import io.restassured.response.Response;
import java.util.List;
import java.util.Map;

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);
    
    // Payload is picked up from test context automatically
    executePost(employeeResourceUrl());

    // To make sure next API call doesn't use the previous request payload
    testContext().reset();
  }

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

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

  @When("user wants to get employee by id {int}")
  public void userWantsToGetEmployeeById(Integer id) {
    Map<String, String> pathParams = Map.of("id", id.toString());
    executeGet(employeeResourceUrl() + "/{id}", pathParams);
  }

  @And("following employee is returned")
  public void followingEmployeeIsReturned(Employee expectedEmployee) {
    final Response response = testContext().getResponse();
    final Employee actualEmployee = response.as(Employee.class);
    assertThat(actualEmployee).isEqualTo(expectedEmployee);
  }

  @When("user wants to get employee by last name containing {string}")
  public void userWantsToGetEmployeeByLastNameContaining(String lastNameContaining) {
    Map<String, String> queryParams = Map.of("last-name-containing", lastNameContaining);
    executeGet(employeeResourceUrl(), null, queryParams);
  }

  @And("following employees are returned")
  public void followingEmployeesAreReturned(List<Employee> expectedEmployees) {
    final Response response = testContext().getResponse();
    final List<Employee> actualEmployees = response.as(new TypeRef<List<Employee>>() {});

    assertThat(actualEmployees).containsExactlyElementsOf(expectedEmployees);
  }

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

Note: In all the previous chapter, we assert the HTTP response code, but here in GET API Scenario, we do another assertion to check if the actual response payload matches with the expected response payload using AssertJ library.

1.3 Modified Common Step Definitions to match ‘Get API’ Response Code Assertion

Look at the the Then Step, it uses Cucumber Expressions alternate text to match both save and get words using save/get.

import static org.assertj.core.api.Assertions.assertThat;

import com.madrascoder.cucumberbooksample.TestContext;
import io.cucumber.java.en.Then;
import io.restassured.response.Response;
import org.springframework.beans.factory.annotation.Autowired;

public class CommonStepDefinitions {

  @Autowired
  private TestContext testContext;

  @Then("the save/get {string}")
  public void theSave(String expectedResult) {
    Response response = testContext.getResponse();
    final int actualStatusCode = response.statusCode();

    if ("IS SUCCESSFUL".equals(expectedResult)) {
      assertThat(actualStatusCode).isIn(200, 201);

    } else if ("FAILS".equals(expectedResult)) {
      assertThat(actualStatusCode).isIn(400, 412);

    } else if ("NOT FOUND".equals(expectedResult)) {
      assertThat(actualStatusCode).isEqualTo(404);

    } else {
      throw new IllegalArgumentException(
          "Expected result is invalid. Valid values are 'IS SUCCESSFUL', 'FAILS', 'NOT FOUND'");
    }
  }
}

Now that we have the Scenarios to test ‘Get APIs’, you may run the test. Test will fail as we have not yet implemented the ‘Get API’, to be specific we have not yet implemented ‘Get employee by last name’.


Step 2: Implement ‘Get APIs’

2.1 Add find method to Employee Repository

Navigate to following location and update the employee repository class,

cd src/main/java/com/madrascoder/cucumberbooksample/repository

Open and add the findByLastName method to the class,

import com.madrascoder.cucumberbooksample.entity.EmployeeEntity;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

@Repository
public interface EmployeeRepository extends JpaRepository<EmployeeEntity, Long> {

  @Query("select e from EmployeeEntity e where e.lastName like %:lastName% order by e.lastName, e.firstName")
  List<EmployeeEntity> findByLastName(String lastName);

}

2.2 Add getById and getByLastName methods to Employee Service

Navigate to following class and add getById and getByLastName methods,

cd src/main/java/com/madrascoder/cucumberbooksample/service

Open add getById and getByLastName methods,

import com.madrascoder.cucumberbooksample.dto.Employee;
import com.madrascoder.cucumberbooksample.entity.EmployeeEntity;
import com.madrascoder.cucumberbooksample.mapper.EmployeeMapper;
import com.madrascoder.cucumberbooksample.repository.EmployeeRepository;
import java.util.List;
import java.util.Optional;
import javax.persistence.EntityNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class EmployeeService {

  private final EmployeeMapper employeeMapper;
  private final EmployeeRepository employeeRepository;

  public EmployeeService(EmployeeMapper employeeMapper, EmployeeRepository employeeRepository) {
    this.employeeMapper = employeeMapper;
    this.employeeRepository = employeeRepository;
  }

  @Transactional
  public Long create(final Employee employee) {
    EmployeeEntity employeeEntity = employeeMapper.toEmployeeEntity(employee);
    employeeEntity = employeeRepository.save(employeeEntity);
    return employeeEntity.getId();
  }

  @Transactional(readOnly = true)
  public Employee getById(final Long id) {
    final Optional<EmployeeEntity> employeeEntityOpt = employeeRepository.findById(id);

    return employeeEntityOpt.map(employeeMapper::toEmployee)
        .orElseThrow(() -> new EntityNotFoundException("Employee not found for given id"));
  }

  @Transactional(readOnly = true)
  public List<Employee> getByLastName(String lastNameContaining) {
    final List<EmployeeEntity> entities =
        employeeRepository.findByLastName(lastNameContaining);

    return employeeMapper.toEmployees(entities);
  }

  @Transactional
  public void update(final Employee employee) {
    final Optional<EmployeeEntity> employeeEntityOpt = employeeRepository.findById(employee.getId());

    final EmployeeEntity employeeEntity = employeeEntityOpt.map(employeeEntityInDb -> {
      employeeMapper.mergeToEmployeeEntity(employee, employeeEntityInDb);
      return employeeEntityInDb;

    }).orElseThrow(() -> new EntityNotFoundException("Employee not found for given id"));

    // No need to explicitly save employee entity as it will be merged when transaction is committed.
    employeeRepository.save(employeeEntity);
  }
}

2.3 Add HTTP GET methods to Employee Rest Controller

Navigate to following location and add GET methods to employee rest controller,

cd src/main/java/com/madrascoder/cucumberbooksample/restapi

Open and add getEmployeeById and getEmployeesByLastName methods,

import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;

import com.madrascoder.cucumberbooksample.dto.Employee;
import com.madrascoder.cucumberbooksample.service.EmployeeService;
import java.net.URI;
import java.util.List;
import javax.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

@RestController
@RequestMapping(path = "/v1/employees")
public class EmployeeRestController {

  private final EmployeeService employeeService;

  // Using Spring Constructor Injection
  public EmployeeRestController(EmployeeService employeeService) {
    this.employeeService = employeeService;
  }

  @PostMapping(consumes = APPLICATION_JSON_VALUE)
  public ResponseEntity<Void> createEmployee(@Valid @RequestBody Employee employee) {
    Long id = employeeService.create(employee);
    URI location = ServletUriComponentsBuilder.fromCurrentRequest()
        .path("/{id}")
        .buildAndExpand(id)
        .toUri();

    return ResponseEntity.created(location)
        .build();
  }

  @GetMapping(path = "/{id}", produces = APPLICATION_JSON_VALUE)
  public ResponseEntity<Employee> getEmployeeById(@PathVariable("id") Long id) {
    Employee employee = employeeService.getById(id);
    return ResponseEntity.ok(employee);
  }

  @GetMapping(produces = APPLICATION_JSON_VALUE)
  public ResponseEntity<List<Employee>> getEmployeesByLastName(
      @RequestParam("last-name-containing") String lastNameContaining) {

    return ResponseEntity.ok(employeeService.getByLastName(lastNameContaining));
  }

  @PutMapping(consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE)
  public ResponseEntity<Employee> updateEmployee(@Valid @RequestBody Employee employee) {
    employeeService.update(employee);
    return ResponseEntity.ok(employeeService.getById(employee.getId()));
  }
}

Step 3: Run the Test

mvn clean verify

Maven Log:

...
Scenario: Get employee by id                             # com/madrascoder/cucumberbooksample/1110-get-employee.feature:18
  Given a employee with following details already exists # com.madrascoder.cucumberbooksample.stepdefinitions.EmployeeStepDefinitions.aEmployeeWithFollowingDetailsAlreadyExists(com.madrascoder.cucumberbooksample.dto.Employee)
  And a employee with following details already exists   # com.madrascoder.cucumberbooksample.stepdefinitions.EmployeeStepDefinitions.aEmployeeWithFollowingDetailsAlreadyExists(com.madrascoder.cucumberbooksample.dto.Employee)
  And a employee with following details already exists   # com.madrascoder.cucumberbooksample.stepdefinitions.EmployeeStepDefinitions.aEmployeeWithFollowingDetailsAlreadyExists(com.madrascoder.cucumberbooksample.dto.Employee)
  When user wants to get employee by id 111002           # com.madrascoder.cucumberbooksample.stepdefinitions.EmployeeStepDefinitions.userWantsToGetEmployeeById(java.lang.Integer)
  Then the get 'IS SUCCESSFUL'                           # com.madrascoder.cucumberbooksample.stepdefinitions.CommonStepDefinitions.theSave(java.lang.String)
  And following employee is returned                     # com.madrascoder.cucumberbooksample.stepdefinitions.EmployeeStepDefinitions.followingEmployeeIsReturned(com.madrascoder.cucumberbooksample.dto.Employee)

Scenario: Get employee by last name                             # com/madrascoder/cucumberbooksample/1110-get-employee.feature:29
  Given a employee with following details already exists        # com.madrascoder.cucumberbooksample.stepdefinitions.EmployeeStepDefinitions.aEmployeeWithFollowingDetailsAlreadyExists(com.madrascoder.cucumberbooksample.dto.Employee)
  And a employee with following details already exists          # com.madrascoder.cucumberbooksample.stepdefinitions.EmployeeStepDefinitions.aEmployeeWithFollowingDetailsAlreadyExists(com.madrascoder.cucumberbooksample.dto.Employee)
  And a employee with following details already exists          # com.madrascoder.cucumberbooksample.stepdefinitions.EmployeeStepDefinitions.aEmployeeWithFollowingDetailsAlreadyExists(com.madrascoder.cucumberbooksample.dto.Employee)
  When user wants to get employee by last name containing 'Gre' # com.madrascoder.cucumberbooksample.stepdefinitions.EmployeeStepDefinitions.userWantsToGetEmployeeByLastNameContainingGre(java.lang.String)
  Then the get 'IS SUCCESSFUL'                                  # com.madrascoder.cucumberbooksample.stepdefinitions.CommonStepDefinitions.theSave(java.lang.String)
  And following employees are returned                          # com.madrascoder.cucumberbooksample.stepdefinitions.EmployeeStepDefinitions.followingEmployeesAreReturned(java.util.List<com.madrascoder.cucumberbooksample.dto.Employee>)
...

Note: First 3 steps printed in the logs for each scenario are from Background. Remember, Background will get executed once for each scenario.


Conclusion

In this chapter, we learnt how to use Background to setup all the data needed to test the ‘Get APIs’, implemented the ‘Get APIs’ and tested them. We also learnt how to validate the data in response payload by comparing it with the expected data given in the feature file.

In the next chapter, we will learn how to generate reports using build-in Cucumber report plugins.


References

AssertJ


Credits

Photo by David Sinclair on Unsplash


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