Let’s write tests using Mockito and JUnit

Spring Boot

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.

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<?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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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

1
2
public interface OrderRepository extends JpaRepository<Order, Long> {
}

Order

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@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.

πŸ‘‰ βœ”οΈ Let’s write a test for the method to fetch Orders.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@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.

1
2
3
4
5
6
7
8
[
    {
        "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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
    "data": [
       {
            "id": 1,
            "buyer": "peter",
            "price": 30.0,
            "qty": 3
        }
    ]
}

Then we should change code like this:

1
2
.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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@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 .

1
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@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 .

1
2
3
4
5
6
7
8
@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! 😍

test class

Write Unit Tests: Service Layer

In this layer, we are going to isolate the service layer and test the service methods. Mock annotation will create a mock object of the Service layer. As previous, here also ExtendWith extension is used to simulate test environment.

1
2
3
4
5
6
7
@ExtendWith(SpringExtension.class)
public class OrderServiceUnitTest {

    @Mock
    private 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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Test
public void testGetOrdersList() {
    Order order1 = new Order(8L, "ben", 80.0, 5);
    Order order2 = new Order(9L, "kevin", 70.0, 2);
    when(orderService.getOrders()).thenReturn(Arrays.asList(order1, order2));
    assertEquals(orderService.getOrders().size(), 2);
    assertEquals(orderService.getOrders().get(0).getBuyer(), "ben");
    assertEquals(orderService.getOrders().get(1).getBuyer(), "kevin");
    assertEquals(orderService.getOrders().get(0).getPrice(), 80.0);
    assertEquals(orderService.getOrders().get(1).getPrice(), 70.0);
    assertNotEquals(orderService.getOrders().get(0).getBuyer(), null);
    assertNotEquals(orderService.getOrders().get(1).getBuyer(), null);
}

πŸ‘‰ βœ”οΈ 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?

1
2
3
4
5
6
7
8
@Test
public void testGetOrderById() {
    Order order = new Order(7L, "george", 60.0, 6);
    when(orderService.getOrderById(7L)).thenReturn(order);
    assertEquals(orderService.getOrderById(7L).getBuyer(), "george");
    assertEquals(orderService.getOrderById(7L).getPrice(), 60.0);
    assertNotEquals(orderService.getOrderById(7L).getBuyer(), null);
}

πŸ‘‰ βœ”οΈ 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.

1
2
3
4
5
6
7
8
@Test
public void testGetInvalidOrderById() {
    when(orderService.getOrderById(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! 😎

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Test
public void testCreateOrder() {
    Order order = new Order(12L, "john", 90.0, 6);
    orderService.createOrder(order);
    verify(orderService, times(1)).createOrder(order);
    ArgumentCaptor<Order> orderArgumentCaptor = ArgumentCaptor.forClass(Order.class);
    verify(orderService).createOrder(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.

1
verify(orderService, times(1)).createOrder(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.

1
verify(orderService).createOrder(orderArgumentCaptor.capture());

The above line will verify that the mocking service will take an Order object and perform the service method. Then I have taken the captor value out of it and compared with the actual value.

1
2
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!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Test
public void testDeleteOrder() {
    Order order = new Order(13L, "simen", 120.0, 10);
    orderService.deleteOrderById(order.getId());
    verify(orderService, times(1)).deleteOrderById(order.getId());
    ArgumentCaptor<Long> orderArgumentCaptor = ArgumentCaptor.forClass(Long.class);
    verify(orderService).deleteOrderById(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! 😍

test class

Let’s write Integration tests guys!!! πŸ’ͺ

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@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.

1
2
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@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! 😍

test class

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 !!!

Reference: https://salithachathuranga94.medium.com/unit-and-integration-testing-in-spring-boot-micro-service-901fc53b0dff