Unit and Integration Testing in Spring Boot Micro Service

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

<?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>
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);
}
}

}
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);
}
}
public interface OrderRepository extends JpaRepository<Order, Long> {
}
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);
}
}
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);
}
}
spring:
datasource:
url: jdbc:mysql://localhost:3306/order-db
username: root
password: ****
jpa:
hibernate:
ddl-auto: update
show-sql: true
generate-ddl: true

Write Unit Tests: Controller Layer

@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 and HandlerMethodArgumentResolver 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.
@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());
}

Mockito When clause

[
{
"id": 1,
"buyer": "peter",
"price": 30.0,
"qty": 3
}
]
{
"data": [
{
"id": 1,
"buyer": "peter",
"price": 30.0,
"qty": 3
}
]
}
.andExpect(jsonPath("$.data", hasSize(1)))
.andExpect(jsonPath("$.data").isArray());
@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());
}
private final ObjectMapper objectMapper = new ObjectMapper();
@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());
}
@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());
}

Write Unit Tests: Service Layer

@ExtendWith(SpringExtension.class)
public class OrderServiceUnitTest {
@Mock
OrderRepository orderRepository;
@InjectMocks
OrderService orderService;

}
@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);
}
@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);
}
@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"));
}
@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());
}
verify(orderRepository, times(1)).save(order);
verify(orderRepository).save(orderArgumentCaptor.capture());
Order orderCreated = orderArgumentCaptor.getValue();
assertEquals("john", orderCreated.getBuyer());
@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);
}

Write Unit Tests: Repository Layer

@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();
}

}
@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");
}
@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");
}
@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());
}
@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");
}

Write Integration Tests

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";
}
}
@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());
}
  • 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.
assertEquals(orderList.size(), orderService.getOrders().size());
assertEquals(orderList.size(), orderRepository.findAll().size());
@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));
}
@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());
}
@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");
}

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Salitha Chathuranga

Salitha Chathuranga

Senior Software Engineer at Sysco LABS | Senior Java Developer | Blogger