Unit and Integration Testing in Spring Boot Micro Service
Let’s write tests using Mockito and JUnit
Hello guys!!! How is your day? I thought of focusing on testing side this time. If you are a good developer, you should be able unit test the functionalities you are implementing. Then developer also can assure that the feature is working fine in a technical manner. This is an additional advantage that we get. So, I’m trying to show you how to write simple unit tests and integration tests for a Spring Boot application.
Unit Tests
- Unit test — in which the smallest testable parts of an application, called units, are individually and independently tested for proper operation.
- We need to perform mocking operations. Not real ones! Reason for that is our unit tests should not affect the other parts of the application. Ex: if we only test the controller layer, it should not affect service layer.
Integration Tests
- Integration test — in which the different units, modules or components of a software application are tested as a combined entity.
- We need to perform real operations to check all the layers are working well in the flow. Here, the layers are controller, service, repository…
Tools used:
- Mockito — mocking framework widely used to mock the operations in a Test-Driven Development(TDD) environment. Works perfectly together with Junit.
- Junit — test assertion framework to write test cases.
Create REST API/Micro Service
Here, I’m going to create a simple REST API to perform Order related functionalities like create Order, get Orders, Delete Orders and etc. I will provide the code for all layers. MySQL database is used to store data in the application.
POM XML
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.rest</groupId>
<artifactId>spring-boot-testing</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-testing</name>
<description>Demo project for Spring Boot Testing</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
OrderController
package com.rest.order.controllers;
import com.rest.order.models.Order;
import com.rest.order.services.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping(value = "/api")
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping(path = "/orders")
public ResponseEntity<List<Order>> getAllOrders() {
return ResponseEntity.ok().body(orderService.getOrders());
}
@PostMapping(path = "/orders")
public ResponseEntity<Order> saveOrder(@RequestBody Order order) {
Order newOrder = orderService.createOrder(order);
return new ResponseEntity<>(newOrder, HttpStatus.CREATED);
}
@GetMapping(path = "/orders/{id}")
public ResponseEntity<Order> getOrderById(@PathVariable Long id) {
return ResponseEntity.ok().body(orderService.getOrderById(id));
}
@DeleteMapping(path = "/orders/{id}")
public ResponseEntity<String> deleteOrderById(@PathVariable Long id) {
boolean deleteOrderById = orderService.deleteOrderById(id);
if (deleteOrderById) {
return new ResponseEntity<>(("Order deleted - Order ID:" + id), HttpStatus.OK);
} else {
return new ResponseEntity<>(("Order deletion failed - Order ID:" + id), HttpStatus.BAD_REQUEST);
}
}
}
OrderService
package com.rest.order.services;
import com.rest.order.exceptions.OrderNotFoundException;
import com.rest.order.models.Order;
import com.rest.order.repositories.OrderRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
public List<Order> getOrders() {
return orderRepository.findAll();
}
public Order getOrderById(Long id) {
return orderRepository.findById(id).orElseThrow(() -> throwException(String.valueOf(id)));
}
public boolean deleteOrderById(Long id) {
Optional<Order> order = orderRepository.findById(id);
if (order.isPresent()) {
orderRepository.deleteById(id);
return true;
} else {
throwException(String.valueOf(id));
return false;
}
}
public Order createOrder(Order order) {
return orderRepository.save(order);
}
private OrderNotFoundException throwException(String value) {
throw new OrderNotFoundException("Order Not Found with ID: " + value);
}
}
OrderRepository
public interface OrderRepository extends JpaRepository<Order, Long> {
}
Order
package com.rest.order.models;
import lombok.*;
import lombok.extern.jackson.Jacksonized;
import javax.persistence.*;
import java.util.Objects;
@Getter
@Setter
@Builder
@ToString
@Jacksonized
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
String buyer;
Double price;
int qty;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Order order = (Order) o;
return qty == order.qty && id.equals(order.id) && buyer.equals(order.buyer) && price.equals(order.price);
}
@Override
public int hashCode() {
return Objects.hash(id, buyer, price, qty);
}
}
OrderNotFoundException
package com.rest.order.exceptions;
public class OrderNotFoundException extends RuntimeException {
public OrderNotFoundException(String s) {
super(s);
}
public OrderNotFoundException(String s, Throwable throwable) {
super(s, throwable);
}
}
application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/order-db
username: root
password: ****
jpa:
hibernate:
ddl-auto: update
show-sql: true
generate-ddl: true
Now our APIs are ready! 😎 Let’s move forward and write tests. 💪
Write Unit Tests: Controller Layer
Since our APIs are ready, we should be having a controller layer. As I told before, while writing unit tests for controller layer, we should make sure that the other layers(repository/service) are not affected. And we have to mock objects!
This is the basic structure of controller test class!
@ExtendWith(SpringExtension.class)
@WebMvcTest(OrderController.class)
public class OrderControllerUnitTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private OrderService orderService;
}
- Using WebMvcTest annotation will disable full auto-configuration and instead apply only configuration relevant to MVC tests (i.e.
@Controller
,@ControllerAdvice
,@JsonComponent
,Converter
/GenericConverter
,Filter
,WebMvcConfigurer
andHandlerMethodArgumentResolver
beans but not@Component
,@Service
or@Repository
beans) — source: Java doc - SpringExtension integrates the Spring TestContext Framework into JUnit 5’s Jupiter programming model.
- MockMVC class is part of Spring MVC test framework which helps in testing the controllers explicitly starting a Servlet container.
- MockBean is used to add mock objects to the Spring application context. This way to can create dummies and perform operations. We need to inject a mock of the Service here to perform a mocking behavior. It will be discussed later.
👉 ✔️ Let’s write a test for the method to fetch Orders.
@Test
public void testGetOrdersList() throws Exception {
when(orderService.getOrders()).thenReturn(Collections.singletonList(order));
mockMvc.perform(get("/api/orders"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$", hasSize(1)))
.andExpect(jsonPath("$").isArray());
}
I will explain line by line here… 😎
Mockito When clause
Mockito provides us a way to simulate the actual behavior. It is formatted like this…
when(something happens).thenReturn(do something)
// OR thenThrow(exception)
Here, we should call order service layer and get orders inside when() clause. That method should return the response we put inside thenReturn(). After this line, this test method will perform a mock operation runtime and prepare a list of orders for the next step.
then we call MockMvc object and perform a GET API call using the relevant URL. We can then bind any number of ResultActions to this API call.
— andDo(print()): Print the result
— andExpect(): Setup expected results in various aspects like response body, response format, response status code and etc.
I have checked these points:
— API is returning 200 code => isOk() method
— Response content is a JSON => content() method
— Response JSON contains an array or not => isArray() method
— Response size is 1 or not => hasSize() method
🔴 Here, “$” means the response JSON root level. Since this GET API is returning response as this, we have to use that notation.
[
{
"id": 1,
"buyer": "peter",
"price": 30.0,
"qty": 3
}
]
If we have the results with a different nested format, we should use relevant keys. Let’s assume we include results inside “data” key.
{
"data": [
{
"id": 1,
"buyer": "peter",
"price": 30.0,
"qty": 3
}
]
}
Then we should change code like this:
.andExpect(jsonPath("$.data", hasSize(1)))
.andExpect(jsonPath("$.data").isArray());
👉 ✔️ Let’s write a test for fetching Order. It will follow the same. Only change would be response format — Object instead of Array.
@Test
public void testGetOrderById() throws Exception {
when(orderService.getOrderById(10L)).thenReturn(order);
mockMvc.perform(get("/api/orders/10"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.buyer", is("andrew")))
.andExpect(jsonPath("$.id", is(10)))
.andExpect(jsonPath("$").isNotEmpty());
}
👉 ✔️ Let’s write a test for creating a new Order. It is also same but this time MockMvc will perform a POST call with a method body! We have to provide a JSON string as body. So we need to convert our Pojo to a JSON string. We can use the Object Mapper from Jackson library.
private final ObjectMapper objectMapper = new ObjectMapper();
I have created POST API to return 201 status code. So, inside Result actions, I used isCreated() — 201 method to match response status instead of isOk() — 200 method.
@Test
public void testCreateOrder() throws Exception {
when(orderService.createOrder(order)).thenReturn(order);
mockMvc.perform(
post("/api/orders")
.content(objectMapper.writeValueAsString(order))
.contentType(MediaType.APPLICATION_JSON)
)
.andDo(print())
.andExpect(status().isCreated())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.buyer", is("andrew")))
.andExpect(jsonPath("$.id", is(10)))
.andExpect(jsonPath("$").isNotEmpty());
}
👉 ✔️ Let’s write a test for the method to delete an Order.
@Test
public void testDeleteOrder() throws Exception {
Order order = new Order(10L, "andrew", 40.0, 2);
when(orderService.deleteOrderById(order.getId())).thenReturn(true);
mockMvc.perform(delete("/api/orders/" + order.getId()))
.andDo(print())
.andExpect(status().isOk());
}
Same thing goes here. Nothing special! Here we return a boolean in the controller method for DELETE. 😎
Now basic test cases for controller are DONE! ❤️
Completed code for controller unit test can be found here: https://github.com/SalithaUCSC/spring-boot-testing/blob/main/src/test/java/com/rest/order/OrderControllerUnitTest.java
Now run the test class and see the results… 💪 All are passing! 😍
Write Unit Tests: Service Layer
In this layer, we are going to isolate the service layer and test the service methods. The “@Mock” annotation will create a mock object of the repository layer. Then “@InjectMocks” will inject this mock into service ;ayer. As previous, here also ExtendWith extension is used to simulate test environment.
@ExtendWith(SpringExtension.class)
public class OrderServiceUnitTest { @Mock
OrderRepository orderRepository; @InjectMocks
OrderService orderService;
}
👉 ✔️ Let’s write a test for the method to fetch Orders.
I have created 2 orders and added into a list. Then I have used Mockito when() clause to mock the behavior.
Next part is different here. We are using Junit Assertions in the service layer tests. We can assert for equality / not equality / NULL scenarios. First parameter is always expected value and the second is actual value.
We do not need MockMvc here since this is one step down from web layer.
@Test
public void testGetOrdersList() {
Order order1 = new Order(8L, "ben", 80.0, 5);
Order order2 = new Order(9L, "kevin", 70.0, 2);
when(orderRepository.findAll()).thenReturn(Arrays.asList(order1, order2));
List<Order> orderList = orderService.getOrders();
assertEquals(orderList.size(), 2);
assertEquals(orderList.get(0).getBuyer(), "ben");
assertEquals(orderList.get(1).getBuyer(), "kevin");
assertEquals(orderList.get(0).getPrice(), 80.0);
assertEquals(orderList.get(1).getPrice(), 70.0);
}
👉 ✔️ Let’s write a service test for the method to fetch Order by ID. Nothing new is there. See the code below. Same stuff!! Isn’t it?
@Test
public void testGetOrderById() {
Order order = new Order(7L, "george", 60.0, 6);
when(orderRepository.findById(7L)).thenReturn(Optional.of(order));
Order orderById = orderService.getOrderById(7L);
assertNotEquals(orderById, null);
assertEquals(orderById.getBuyer(), "george");
assertEquals(orderById.getPrice(), 60.0);
}
👉 ✔️ Let’s write a service test for fetching invalid Order. There we have to expect a OrderNotFoundException since the ID is not available in the DB. This is a negative test case. Here, assertThrows() can be used to throw an exception deliberately and compare it with the actual value in the exception message. I have checked the string message content equality.
@Test
public void testGetInvalidOrderById() {
when(orderRepository.findById(17L)).thenThrow(new OrderNotFoundException("Order Not Found with ID"));
Exception exception = assertThrows(OrderNotFoundException.class, () -> {
orderService.getOrderById(17L);
});
assertTrue(exception.getMessage().contains("Order Not Found with ID"));
}
👉 ✔️ Let’s write a service test for creating a new Order. In this scenario, there are some new things to learn! 😎
@Test
public void testCreateOrder() {
Order order = new Order(12L, "john", 90.0, 6);
orderService.createOrder(order);
verify(orderRepository, times(1)).save(order);
ArgumentCaptor<Order> orderArgumentCaptor = ArgumentCaptor.forClass(Order.class);
verify(orderRepository).save(orderArgumentCaptor.capture());
Order orderCreated = orderArgumentCaptor.getValue();
assertNotNull(orderCreated.getId());
assertEquals("john", orderCreated.getBuyer());
}
Here I have omitted the when() clause like we did before. We just need to call the method since the create method is not returning anything(void).
Mockito provides verify() method to assure the behavior of a method call.
verify(orderRepository, times(1)).save(order);
The above line may verify that the method is called only once!
ArgumentCaptor is used to capture arguments for mocked methods. Since the POST API call is returning an order object, I have provided ArgumentCaptor type as Order.
verify(orderRepository).save(orderArgumentCaptor.capture());
The above line will verify that the mocking service will take an Order object and perform the repository method. Then I have taken the captor value out of it and compared with the actual value.
Order orderCreated = orderArgumentCaptor.getValue();
assertEquals("john", orderCreated.getBuyer());
👉 ✔️ Let’s write a service test for deleting an Order. Same sort of code is followed here also like we tested POST service method. Here, the type of ArgumentCaptor is taken as Long since the service method is accepting a Long ID. AS we did before, again captor value is compared with actual!
@Test
public void testDeleteOrder() {
Order order = new Order(13L, "simen", 120.0, 10);
when(orderRepository.findById(13L)).thenReturn(Optional.of(order));
orderService.deleteOrderById(order.getId());
verify(orderRepository, times(1)).deleteById(order.getId());
ArgumentCaptor<Long> orderArgumentCaptor = ArgumentCaptor.forClass(Long.class);
verify(orderRepository).deleteById(orderArgumentCaptor.capture());
Long orderIdDeleted = orderArgumentCaptor.getValue();
assertNotNull(orderIdDeleted);
assertEquals(13L, orderIdDeleted);
}
Now basic test cases for service are DONE! ❤️
Completed code for service unit test can be found here: https://github.com/SalithaUCSC/spring-boot-testing/blob/main/src/test/java/com/rest/order/OrderServiceUnitTest.java
Now run the test class and see the results… 💪 All are passing! 😍
Let’s write Integration tests guys!!! 💪
Write Unit Tests: Repository Layer
Here, we will be following a different way. We are not going to deal with real database through the repository. It will affect the real data…So, we have an alternative way to setup our repository tests with a test database! Look at the below setup of the class.
@DataJpaTest
@ExtendWith(SpringExtension.class)
@AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.H2)
public class OrderRepositoryUnitTest {
@Autowired
OrderRepository orderRepository;
@BeforeEach
public void setUp() {
orderRepository.save(new Order(100L, "jane", 200.0, 2));
orderRepository.save(new Order(200L, "ben", 100.0, 5));
}
@AfterEach
public void destroy() {
orderRepository.deleteAll();
}
}
“@DataJpaTest” is providing the test environment for JPA layer. Then I have used this annotation — AutoConfigureTestDatabase with EmbeddedDatabaseConnection.H2. This means our tests are connected automatically configured a H2 in memory database! Since we are using MySQL relational DB, this is good for testing. Then I have autowired the repository object without mocking — not an issue because we are not using the real database. So, no real data will be saved even the real repository methods are called!
👉 ✔️ Let’s write a test for the method to fetch Orders from repository.
@Test
public void testGetAllOrders() {
List<Order> orderList = orderRepository.findAll();
Assertions.assertThat(orderList.size()).isEqualTo(2);
Assertions.assertThat(orderList.get(0).getId()).isNotNegative();
Assertions.assertThat(orderList.get(0).getId()).isGreaterThan(0);
Assertions.assertThat(orderList.get(0).getBuyer()).isEqualTo("jane");
}
Since we are saving 2 Order objects using “@BeforeEach”, our test database is having 2 orders already. Then I have checked the results with Assertions class methods. AssertThat is really handy with its inbuilt set of methods like isEqualTo / isGreaterThan / isNotNegative and etc…
👉 ✔️ Let’s write a service test for fetching invalid Order. Since this is repository layer, it won’t throw OrderNotFoundException! Remember? We threw it in the service layer! But here, we have to expect a NoSuchElementException while taking a non existing value from a Optional object.
@Test
public void testGetInvalidOrder() {
Exception exception = assertThrows(NoSuchElementException.class, () -> {
orderRepository.findById(120L).get();
});
Assertions.assertThat(exception).isNotNull();
Assertions.assertThat(exception.getClass()).isEqualTo(NoSuchElementException.class);
Assertions.assertThat(exception.getMessage()).isEqualTo("No value present");
}
👉 ✔️ Let’s write a service test for creating a new Order. Here we just need to compare the returning value from the saved object. If they are fine, then save method is working fine!
@Test
public void testGetCreateOrder() {
Order saved = new Order(300L, "tim", 50.0, 4);
Order returned = orderRepository.save(saved);
Assertions.assertThat(returned).isNotNull();
Assertions.assertThat(returned.getBuyer()).isNotEmpty();
Assertions.assertThat(returned.getId()).isGreaterThan(1);
Assertions.assertThat(returned.getId()).isNotNegative();
Assertions.assertThat(saved.getBuyer()).isEqualTo(returned.getBuyer());
}
👉 ✔️ Let’s write a service test for deleting an Order. I have checked for an exception after deleting an Order. So, we should be getting that exception when we find the same object after deletion. That was the purpose!
@Test
public void testDeleteOrder() {
Order saved = new Order(400L, "ron", 60.0, 3);
orderRepository.save(saved);
orderRepository.delete(saved);
Exception exception = assertThrows(NoSuchElementException.class, () -> {
orderRepository.findById(400L).get();
});
Assertions.assertThat(exception).isNotNull();
Assertions.assertThat(exception.getClass()).isEqualTo(NoSuchElementException.class);
Assertions.assertThat(exception.getMessage()).isEqualTo("No value present");
}
Now basic test cases for repository are DONE! ❤️
Completed code for repository unit test can be found here: https://github.com/SalithaUCSC/spring-boot-testing/blob/main/src/test/java/com/rest/order/OrderRepositoryUnitTest.java
Now run the test class and see the results… 💪 All are passing! 😍
Write Integration Tests
As I mentioned before, this is not like a Unit test. We have to change the whole approach in this scenario. The purpose of writing integration test for our order service is making sure that order related functionalities are working fine connecting all the layers in the flow. Layers will be controller, service, repository, entity, exceptions configurations and etc.
So we cannot mock here…right! We have to do some real operations. Let’s setup the class for this first.
package com.rest.order;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.rest.order.models.Order;
import com.rest.order.repositories.OrderRepository;
import com.rest.order.services.OrderService;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.*;
import org.springframework.test.context.jdbc.Sql;
import java.util.List;
import java.util.Objects;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class OrderApiIntegrationTest {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private OrderRepository orderRepository;
@Autowired
private OrderService orderService;
private static HttpHeaders headers;
private final ObjectMapper objectMapper = new ObjectMapper();
@BeforeAll
public static void init() {
headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
} private String createURLWithPort() {
return "http://localhost:" + port + "/api/orders";
}}
SpringBootTest annotation loads the complete Spring application context and provides a mock web environment. I have given an additional condition for that to start the mock web environment on a random port!
LocalServerPort annotation is used to bind that port to the API URL. Then I have constructed the URL in a separate method which can be reused in the whole class.
Now I have injected the real service and repository layers — not like previous. I have used Autowired annotation instead Mock or MockBean!
TestRestTemplate is the testing version class of RestTemplate class. If you don’t what is the purpose of Rest Template, please read this: https://medium.com/@salithachathuranga94/rest-template-with-spring-boot-e2001a8219e6
We can simply use TestRestTemplate to perform API calls via the entire application!
👉 ✔️ Let’s write an integration test for the method to fetch Orders.
@Test
@Sql(statements = "INSERT INTO orders(id, buyer, price, qty) VALUES (2, 'john', 24, 1)", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(statements = "DELETE FROM orders WHERE id='2'", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
public void testOrdersList() {
HttpEntity<String> entity = new HttpEntity<>(null, headers);
ResponseEntity<List<Order>> response = restTemplate.exchange(
createURLWithPort(), HttpMethod.GET, entity, new ParameterizedTypeReference<List<Order>>(){});
List<Order> orderList = response.getBody();
assert orderList != null;
assertEquals(response.getStatusCodeValue(), 200);
assertEquals(orderList.size(), orderService.getOrders().size());
assertEquals(orderList.size(), orderRepository.findAll().size());
}
Something new is here right? 😎
You should see “Sql” annotation. What is the purpose of it? Well…I told you we are going to perform real actions. So, we should make sure that our database is not polluted after running an integration test by anyone in the dev team! Data should not be changed! We can use this annotation for each test method and tell Spring Boot that we need to execute some manual SQL commands while running the test.
Ex: If we create a new Order while testing POST API call, what will happen? Unexpected data is there right? Then we have changed the original data! It’s not correct…So what we should do? After running the test case, we should delete that order! Simple! 😃
I have used 2️⃣ SQL commands to create and remove each test object. We can provide a parameter called executionPhase. It will take cake at what stage application should perform the SQL command. So I have used BEFORE_TEST_METHOD for SAVE and AFTER_TEST_METHOD for DELETE.
- I have used exchange method in TestRestTeamplate class. In this method, I’m expecting a List of Orders.
- Here, HttpEntity object is needed to send as a parameter. Since GET call does not need a body, I have created it with NULL body.
- Headers have been initialized inside BeforeAll test annotation.
Then we just need to compare response with direct method call to service and repository layers. Status code also checked whether its 200 or not. Then we can guarantee that if anyone call the API from external source, it will give the correct result as same as repository and service individually gives.
assertEquals(orderList.size(), orderService.getOrders().size());
assertEquals(orderList.size(), orderRepository.findAll().size());
Since I have inserted only 1 object using SQL command, this test will assure that the response size is 1.
👉 ✔️ Let’s write an integration test for the method to fetch Order by ID. We need to modify the URL since now it’s taking a path variable! Same as previous, we need to call through rest and compare the response with service, repository method calls.
@Test
@Sql(statements = "INSERT INTO orders(id, buyer, price, qty) VALUES (20, 'sam', 50, 4)", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(statements = "DELETE FROM orders WHERE id='20'", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
public void testOrderById() throws JsonProcessingException {
HttpEntity<String> entity = new HttpEntity<>(null, headers);
ResponseEntity<Order> response = restTemplate.exchange(
(createURLWithPort() + "/20"), HttpMethod.GET, entity, Order.class);
Order orderRes = response.getBody();
String expected = "{\"id\":20,\"buyer\":\"sam\",\"price\":50.0,\"qty\":4}";
assertEquals(response.getStatusCodeValue(), 200);
assertEquals(expected, objectMapper.writeValueAsString(orderRes));
assert orderRes != null;
assertEquals(orderRes, orderService.getOrderById(20L));
assertEquals(orderRes.getBuyer(), orderService.getOrderById(20L).getBuyer());
assertEquals(orderRes, orderRepository.findById(20L).orElse(null));
}
👉 ✔️ Let’s write an integration test for the method to create a new Order. In this case we use a POST call. Then we have to provide a method body. There we have to update HttpEntity with the order object converted into a JSON string.
And we don’t need 2️⃣ SQL commands. Why? Because we are creating and saving an object inside method itself. So, we just need to delete it after test method is executed.
@Test
@Sql(statements = "DELETE FROM orders WHERE id='3'", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
public void testCreateOrder() throws JsonProcessingException {
Order order = new Order(3L, "peter", 30.0, 3);
HttpEntity<String> entity = new HttpEntity<>(objectMapper.writeValueAsString(order), headers);
ResponseEntity<Order> response = restTemplate.exchange(
createURLWithPort(), HttpMethod.POST, entity, Order.class);
assertEquals(response.getStatusCodeValue(), 201);
Order orderRes = Objects.requireNonNull(response.getBody());
assertEquals(orderRes.getBuyer(), "peter");
assertEquals(orderRes.getBuyer(), orderRepository.save(order).getBuyer());
}
👉 ✔️ Let’s write an integration test for the method to delete an Order. Test should be written using DELETE type in the exchange method. We need to modify the URL since now it’s taking a path variable! I’m returning a string in controller layer for delete method. So, I have checked that string for verification using JUnit.
@Test
@Sql(statements = "INSERT INTO orders(id, buyer, price, qty) VALUES (6, 'alex', 75, 3)", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(statements = "DELETE FROM orders WHERE id='6'", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
public void testDeleteOrder() {
ResponseEntity<String> response = restTemplate.exchange(
(createURLWithPort() + "/6"), HttpMethod.DELETE, null, String.class);
String orderRes = response.getBody();
assertEquals(response.getStatusCodeValue(), 200);
assertNotNull(orderRes);
assertEquals(orderRes, "Order deleted - Order ID:6");
}
Now all the integration cases for our micro service are COMPLETED! ❤️
Completed code for integration test can be found here: https://github.com/SalithaUCSC/spring-boot-testing/blob/main/src/test/java/com/rest/order/OrderServiceUnitTest.java
Now run the integration test class and see the results… 💪 All test case should be passing! 😍
Job done…right? If you missed anything, please grad the code from GitHub links. 😃
You may feel that the article is lengthy. But I tried my best to keep it short. All the important things were explained also. Nowadays, writing unit test at least, is a required action we have to perform. It leads us to create a robust and well functioning application! 💪 So, try to write tests as much as possible for your implementations. There may be some different ways also to write tests. If you know them, share with also. 😎
Let me know if there are any issues!
Bye bye !!!