Create a REST Controller

Create a REST Controller

Spring Application Deployed with Kubernetes

Step by step building an application using Spring Boot and deployed via Docker on Kubernetes with Helm

full course
  1. Setup: IDE and New Project
  2. Create the Data Repository
  3. Building a Service Layer
  4. Create a REST Controller
  5. Logging, Tracing and Error Handling
  6. Documentation and Code Coverage
  7. Database as a Service
  8. Containerize the Service With Docker
  9. Docker Registry
  10. Automated Build Pipeline
  11. Helm for Deployment
  12. Setting up a Kubernetes Cluster
  13. Automating Deployment (for CICD)
  14. System Design
  15. Messaging and Event Driven Design
  16. Web UI with React
  17. Containerizing our UI
  18. UI Build Pipeline
  19. Put the UI in to Helm
  20. Creating an Ingress in Kubernetes
  21. Simplify Deployment
  22. Conclusion and Review

In this article we’re going to take the service layer we created in the last step and add a REST interface to it. We’re going to follow the hexagonal design principles we previously discussed and add a functional or component test and explain how those test work and why they’re important.

Create the Controller and DTO

Create the DTO

Create a package called com.brianrook.medium.customer.controller.dto. Create a class called CustomerDTO and add this content:

package com.brianrook.medium.customer.controller.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CustomerDTO {
    private Long customerId;
    @NotBlank
    @Size(min = 0, max = 100)
    private String firstName;
    @NotBlank
    @Size(min = 0, max = 100)
    private String lastName;
    @Pattern(regexp="\\(\\d{3}\\)\\d{3}-\\d{4}", message = "Phone number must match the pattern (###)###-####")
    @Size(max = 20)
    private String phoneNumber;
    @Email
    @Size(max = 150)
    private String email;
}

You can see here that we’ve added javax validation annotations to the DTO. This will signal to spring to trap and return 400 on invalid data.

Service Configuration

We also need to add some configuration. In src/main/resources create an application.yaml (if it doesn’t exist. Delete any application.properties or bootstrap.properties)

server:
  port: 10000
spring:
  application:
    name: customer
  jackson:
    deserialization:
      FAIL_ON_UNKNOWN_PROPERTIES: false

Here we’re going to tell spring to start up on port 10000 refer to this service as customer and also setup the jackson deserializer to ignore any fields that are either not sent or don’t map to fields in our DTO.

Create the Controller

Create a class called CustomerController in com.brianrook.medium.customer.controller. Add this content:

package com.brianrook.medium.customer.controller;

import com.brianrook.medium.customer.controller.dto.CustomerDTO;
import com.brianrook.medium.customer.service.CustomerService;
import com.brianrook.medium.customer.service.model.Customer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;

import java.net.URISyntaxException;

@Controller
@RequestMapping(value = "/customer")
@Slf4j
public class CustomerController {
    @Autowired
    private CustomerService customerService;

    @PostMapping(value = "/",
            consumes = MediaType.APPLICATION_JSON_VALUE,
            produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<CustomerDTO> saveCustomer(
            @RequestBody @Valid CustomerDTO customerDTO) throws URISyntaxException {
        Customer customer = CustomerDTOMapper.INSTANCE.customerDTOToCustomer(customerDTO);

        Customer savedCustomer = customerService.saveCustomer(customer);

        CustomerDTO savedCustomerDTO = CustomerDTOMapper.INSTANCE.customerToCustomerDTO(savedCustomer);
        return new ResponseEntity<CustomerDTO>(savedCustomerDTO, HttpStatus.CREATED);
    }
}

Create DTO Mapper

This isn’t going to compile yet, because we need to create a mapper for the DTO to the model object. Lets create a new package in com.brianrook.medium.customer.controller.mapper and add an interface called CustomerDTOMapper into that package with this content:

package com.brianrook.medium.customer.controller.mapper;

import com.brianrook.medium.customer.controller.dto.CustomerDTO;
import com.brianrook.medium.customer.service.model.Customer;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

@Mapper
public interface CustomerDTOMapper {
    CustomerDTOMapper INSTANCE = Mappers.getMapper(CustomerDTOMapper.class);

    CustomerDTO customerToCustomerDTO(Customer customer);

    Customer customerDTOToCustomer(CustomerDTO customerEntity);
}

Import that class into the controller and make sure it compiles.

Give it a Floor Run

Lets start the application and test it with postman. Right click on MediumCustomerApplication, choose Run ‘MediumCustomer…main() and confirm that you can see the application startup succesfully:

2020-03-28 13:28:28.773  INFO [customer,,,] 20504 --- [  restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 10000 (http) with context path ''
2020-03-28 13:28:29.081  INFO [customer,,,] 20504 --- [  restartedMain] c.b.m.c.MediumCustomerApplication        : Started MediumCustomerApplication in 9.781 seconds (JVM running for 10.525)

You can use postman to make a post. Here’s the request:

POST /customer/ HTTP/1.1
Host: localhost:10000
Content-Type: application/json
{
 "firstName": "Brian",
 "lastName":  "Rook",
 "phoneNumber": "(303)555-1212",
 "email": "[email protected]"
}

which should respond with:

201 Created
{
"customerId": 1,
"firstName": "Brian",
"lastName": "Rook",
"phoneNumber": "(303)555-1212",
"email": "[email protected]"
}

Write a Functional Test

In the test directory create a new java package for com.brianrook.medium.customer.controller. In that package create a class called CustomerControllerTest. Add this content:

@Autowired
private OrganizerDAO organizerDAO;
@LocalServerPort
int randomServerPort;


String organizerPath = "/organizer/";

@BeforeEach
public void setUp(){
    organizerDAO.deleteAll();
}

@Test
public void testAddCustomerSuccess() throws URISyntaxException
{
    RestTemplate restTemplate = new RestTemplate();
    String baseUrl = "http://localhost:"+randomServerPort+organizerPath;
    URI uri = new URI(baseUrl);
    OrganizerDTO organizerDTO = OrganizerDTO.builder()
            .firstName("test")
            .lastName("user")
            .email("[email protected]")
            .phoneNumber("(123)654-7890")
            .build();

    HttpHeaders headers = new HttpHeaders();
    headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);

    HttpEntity<OrganizerDTO> request = new HttpEntity<>(organizerDTO, headers);

    ResponseEntity<OrganizerDTO> result = restTemplate.postForEntity(uri, request, OrganizerDTO.class);

    //Verify request succeed
    Assertions.assertEquals(201, result.getStatusCodeValue());
    Long newOrganizerId = result.getBody().getOrganizerId();
    Assertions.assertTrue(newOrganizerId!=null && newOrganizerId>0);

    //verify the state of the database
    Optional<CustomerEntity> storedEntityOptional = customerDAO.findById(newCustomerId);
assertTrue(storedEntityOptional.isPresent());
}

In this test we’re telling spring to start up an application context. In that context: start up our web container on a random port. Then we’re making a restTemplate that will make a request to our endpoint (create customer). We then verify the response is correct, but we also make sure that the database has the record we expect.

Now we can run the test and get a success. However, we should also run code coverage at this point. In the test configuration window, go to the Code Coverage tab and change the packages to include in coverage data to reflect our entire package.

Run the tests again with coverage enabled and review the report. You should see that the only code that wasn’t covered was some of the Lombok constructors (because we used them in our tests). We’ll fix this later when we add code coverage to our maven build process.

Why Functional Testing is Better than Integration Testing

In previous projects, on different teams, we used a different paradigm of writing tests for each class which involved writing lots of mock objects. There are several problems with these types of tests:

  • You write a lot more tests
  • They chain you to your current design/implementation
  • The mocks introduce uncertainty around expected behavior vs actual behavior

Although writing lots of tests is tedious, its not the biggest drain to productivity. In reality, being chained to your current design and the uncertainty of the behavior introduces A LOT of risk to your development effort. For example, introducing a new field in the database would mean making the change to the database and the DAO. Then updating the mock that is used for the service test and then updating the service test. This cascade of changes is a huge drag on your development effort. This doesn’t even cover the concerns related to adding a feature and finding out the tests break. Now you have to decipher the old tests, their mocks as well as the actual service code. It becomes a nightmare.

Functional tests reduce the number of tests you need and can achieve the same amount of code coverage. Additionally, they are driven by testing behavior and not implementation. This means that you can change your internal design all you want. As long as the ‘edges’ of your system don’t change your tests will still pass.

Since there are no dependencies outside of our application (we’re not using a real database or external rest client), we can run this type of test at maven build time as a ‘unit’ test.

Functional testing gives us a high level of confidence about the behavior of our system, it gives us a high level of code coverage, we don’t spend a lot of time writing tests, we have flexibility to change our system… the advantages are numerous.

Build and Commit

Lets go ahead and verify our application works and then push up our changes.

git checkout -b controller
mvn clean install
git add .
git commit -m "controller and tests"
git push
git checkout master
git merge controller
git push

0 comments on “Create a REST ControllerAdd yours →

Leave a Reply

Your email address will not be published.