Spring Application Deployed with Kubernetes
Step by step building an application using Spring Boot and deployed via Docker on Kubernetes with Helm
full course- Setup: IDE and New Project
- Create the Data Repository
- Building a Service Layer
- Create a REST Controller
- Logging, Tracing and Error Handling
- Documentation and Code Coverage
- Database as a Service
- Containerize the Service With Docker
- Docker Registry
- Automated Build Pipeline
- Helm for Deployment
- Setting up a Kubernetes Cluster
- Automating Deployment (for CICD)
- System Design
- Messaging and Event Driven Design
- Web UI with React
- Containerizing our UI
- UI Build Pipeline
- Put the UI in to Helm
- Creating an Ingress in Kubernetes
- Simplify Deployment
- 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 Controller”Add yours →