Testing Spring Boot RESTful Services

Testing Spring Boot RESTful Services

A Spring Boot RESTful service is typically divided into three layers:  Repository, Service, and Controller. This layering helps to segregate the RESTful application responsibilities and enabling loose coupling between the objects.

When you develop a layered RESTful application, you will also need to test the different layers.

In this post, I will discuss testing Spring Boot RESTful Services with Spring MVC Test and JUnit5.

The Maven POM

To start testing Spring Boot RESTful services, you need spring-boot-starter-test, which is a starter dependency for Spring Testing.

This Spring Boot starter depenency also transitively brings in other testing dependencies such as Mockito, JUnit, Hamcrest, AssertJ.

This is the dependency you need to add in the pom.xml file.

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-test</artifactId>
     <scope>test</scope>
     <exclusions>
         <exclusion>
             <groupId>org.junit.vintage</groupId>
             <artifactId>junit-vintage-engine</artifactId>
         </exclusion>
     </exclusions>
</dependency>

Testing the Repository Layer

This is the repository I will be testing.

@Repository
public interface ProductRepository extends JpaRepository <Product, Integer> {

}

Let’s start writing the test class, ProductRepositoryTest .

@ExtendWith(SpringExtension.class)
@DataJpaTest
class ProductRepositoryTest {

The first statement annotates the class with @ExtendWith(SpringExtension.class).  This integrates the Spring test context framework into the JUnit 5 Jupiter programming model.

Our test will be an integration test as an external database is used. Being an integration test, we need to load the Spring context in our test. We can do that using the @SpringBootTest annotation.

However, loading the entire Spring context is heavy and makes the tests slow.

Therefore, we will only load the Spring Data JPA slice of the Spring context. The @DataJpaTest annotation in the code does exactly that.

Next, let’s autowire the ProductRepository that we will test and write the setup() and teardown() methods.

    @Autowired
    private ProductRepository productRepository;
    private Product product;

    @BeforeEach
    public void setUp() {

        product = new Product(1,"Bat",2500);
    }

    @AfterEach
    public void tearDown() {
        productRepository.deleteAll();
        product = null;
    }

Test Case for Saving a Product

Let’s start writing a test for saving a product.

The test code is this.

@Test
public void givenProductToAddShouldReturnAddedProduct(){

     productRepository.save(product);
     Product fetchedProduct = productRepository.findById(product.getId()).get();

     assertEquals(1, fetchedProduct.getId());
}

Test Case to Retrieve the List of Products

This test code tests for the retrieval of all products.

@Test
public void GivenGetAllProductShouldReturnListOfAllProducts(){
     Product product1 = new Product(1,"ball",400);
     Product product2 = new Product(2,"bat",500);
     productRepository.save(product1);
     productRepository.save(product2);

     List<Product> productList = (List<Product>) productRepository.findAll();
     assertEquals("bat", productList.get(1).getName());

}

Test Case to Retrieve Product by Id

This test code tests for retrieving a product by ID.

@Test
public void givenIdThenShouldReturnProductOfThatId() {
     Product product1 = new Product(1,"bat",3000);
     Product product2 = productRepository.save(product1);

     Optional<Product> optional =  productRepository.findById(product2.getId());
     assertEquals(product2.getId(), optional.get().getId());
     assertEquals(product2.getName(), optional.get().getName());
}

Test Case to Delete a Product by Id

Finally, this test code tests for the deletion of products.

@Test
public void givenIdTODeleteThenShouldDeleteTheProduct() {
      Product product = new Product(4,  "pen",160);
      productRepository.save(product);
      productRepository.deleteById(product.getId());
      Optional optional = productRepository.findById(product.getId());
      assertEquals(Optional.empty(), optional);
}

Let’s run the tests, as you can see from the output provided below, all the test case passes.

test cases for repository

Testing the Service Layer

The Service layer class ProductServiceImpl is responsible for using the repository for performing CRUD operation.

This is the code of the ProductServiceImpl class.

ProductServiceImpl.java

@Service
public class ProductServiceImpl implements ProductService{

    private ProductRepository productRepository;

    @Autowired
    public void setProductRepository(ProductRepository productRepository){
          this.productRepository =productRepository;
    }

    @Override
    public Product addProduct(Product product) throws ProductAlreadyExistsException {
        if(productRepository.existsById(product.getId())){
            throw new ProductAlreadyExistsException();
        }
        return productRepository.save(product);
    }

    @Override
    public List<Product> getAllProducts() {

        return (List<Product>) productRepository.findAll();
    }

    @Override
    public Product getProductByid(int id) {
        return productRepository.findById(id).orElse(null);
    }

    @Override
    public Product deleteProductById(int id) {
        Product product = null;
        Optional optional = productRepository.findById(id);
        if (optional.isPresent()) {
            product = productRepository.findById(id).get();
            productRepository.deleteById(id);
        }
        return product;
    }

We will write pure unit tests of the service implementation – ProductServiceImpl. The reason is that unit tests are super-fast and therefore cuts down developers’ time.

Note that in unit testing, when we have external dependencies, we mock the dependencies.  So in this example, we will mock the ProductRepository class.

For more information on mocking, refer to my post Mocking in Unit Tests with Mockito.

Let’s start writing the code. The code for the unit test is this.

@ExtendWith(MockitoExtension.class)
class ProductServiceTest {


    @Mock
    private ProductRepository productRepository;

    @Autowired
    @InjectMocks
    private ProductServiceImpl productService;
    private Product product1;
    private Product product2;
    List<Product> productList;

    @BeforeEach
    public void setUp() {
    productList = new ArrayList<>();
    product1 = new Product(1, "bread",20);
    product2 = new Product(2, "jam",200);
    productList.add(product1);
    productList.add(product2);
    }

    @AfterEach
    public void tearDown() {
    product1 = product2 = null;
    productList = null;
    }

Line 1 uses annotation. This MockitoExtension is a part of the Mockito library that is used to perform mocking. It initializes mocks in test classes.

Then, Line5 – Line 6 uses the @Mock annotation on ProductRepository. At run time, Mockito will create a mock of ProductRepository.

Finally, Line 8-Line10 uses the @Autowired annotation to autowire in ProductServiceImpl . The @InjectMock the annotation will initialize the  ProductServiceImpl  object with the ProductRepository mock.

Test Case for Saving a Product

The test code for saving a product is this.

@Test
void givenProductToAddShouldReturnAddedProduct() throws ProductAlreadyExistsException{

     //stubbing
     when(productRepository.save(any())).thenReturn(product1);
     productService.addProduct(product1);
     verify(productRepository,times(1)).save(any());

}

Test Code for Retrieval of all Products

@Test
public void GivenGetAllUsersShouldReturnListOfAllUsers(){
     productRepository.save(product1);
    //stubbing mock to return specific data
    when(productRepository.findAll()).thenReturn(productList);
    List<Product> productList1 =productService.getAllProducts();
    assertEquals(productList1,productList);
    verify(productRepository,times(1)).save(product1);
    verify(productRepository,times(1)).findAll();
}

Test Case to Retrieve a Product by Id

The test code which tests for retrieving a product by ID is this.

@Test
public void givenIdThenShouldReturnProductOfThatId() {
   Mockito.when(productRepository.findById(1)).thenReturn(Optional.ofNullable(product1));
   assertThat(productService.getProductByid(product1.getId())).isEqualTo(product1);
}

Test Case to Delete a Product by Id

The test code for deleting a product of the respective id.

@Test
public void givenIdTODeleteThenShouldDeleteTheProduct(){
    when(productService.deleteProductById(product1.getId())).thenReturn(product1);
//assertThat(productService.);
    verify(productRepository,times(1)).findAll();

}

Let’s run the tests.

As you can see from the output provided below, all the test cases pass.

junit test cases for service

Testing the Controller Layer

We will also write a pure unit test for the controller.

The code of the ProductController.java class that we will test is this.

@RestController
@RequestMapping("api/v1")
public class ProductController {
    private ProductService productService;

    public ProductController(ProductService productService) {
        this.productService = productService;
    }

    @PostMapping("product")
    public ResponseEntity<Product> addProduct(@RequestBody Product product) throws ProductAlreadyExistsException {
        Product saveProduct = productService.addProduct(product);
        return new ResponseEntity<>(saveProduct, HttpStatus.CREATED);
    }

    @GetMapping("products")
    public ResponseEntity<List<Product>> getAllProducts(){

        return new ResponseEntity<List<Product>>(
                (List <Product>) productService.getAllProducts(),HttpStatus.OK);
    }

    @GetMapping("product/{id}")
    public ResponseEntity<Product> getProductById(@PathVariable("id") int id){
        return new ResponseEntity<>(productService.getProductByid(id),HttpStatus.OK);
    }


    @DeleteMapping("product/{id}")
        public ResponseEntity<Product> deleteProduct(@PathVariable("id") int id) {
        ResponseEntity responseEntity;
        Product deletedProduct = productService.deleteProductById(id);
        responseEntity = new ResponseEntity<Product>(deletedProduct, HttpStatus.OK);

        return responseEntity;
   }

}

As you can see in the preceding code, the controller has a dependency on the service class, ProductService.

So in our test, we will use Mockito to mock ProductService and inject a mock on ProductController.

Now, let’s start writing the test class.

@ExtendWith(MockitoExtension.class)
class ProductControllerTest {

   @Mock
   private ProductService productService;
   private Product product;
   private List<Product> productList;

   @InjectMocks
   private ProductController productController;

   @Autowired
   private MockMvc mockMvc;

   @BeforeEach
   public void setup(){
   product = new Product(1,"ball",670);
   mockMvc = MockMvcBuilders.standaloneSetup(productController).build();
   }

   @AfterEach
   void tearDown() {
   product = null;
}

Line 4-Line5 uses the @Mock annotation on ProductService . At run time, Mockito will create a mock of ProductService.

Next, Line 12-Line13 uses the @Autowired annotation to autowire in MockMvc. The @InjectMock annotation will initialize the  ProductController object.

We need to send HTTP requests to the controller from our test class to assert they are responding as expected. For that, Line 18 uses  MockMvc.

MockMvc provides a powerful way to mock Spring MVC.  Through @MockMvc you can send MockHttp request to a controller and test how the controller responds.

You can create instance of mockMvc  through two methods of MockMvcBuilders. I have used standaloneSetup which registers the controller instances.  The other one is the webappContextSetup method.

Test Case to Post a Product

Let’s write a test for posting a product.

@Test
public void PostMappingOfProduct() throws Exception{
   when(productService.addProduct(any())).thenReturn(product);
   mockMvc.perform(post("/api/v1/product").
           contentType(MediaType.APPLICATION_JSON).
           content(asJsonString(product))).
           andExpect(status().isCreated());
   verify(productService,times(1)).addProduct(any());
}

In-Line 4-Line 7, mockMvc performs a post-operation of product on the URL "/api/v1/product"   whose content type is  APPLICATION_JSON.  The status is isCreated().

Test Case to Retrieve all Products

This test uses mockMvc to send a GET request to retrieve all products

@Test
public void GetMappingOfAllProduct() throws Exception {
    when(productService.getAllProducts()).thenReturn(productList);
    mockMvc.perform(MockMvcRequestBuilders.get("/api/v1/products").
                   contentType(MediaType.APPLICATION_JSON).
                   content(asJsonString(product))).
                   andDo(MockMvcResultHandlers.print());
    verify(productService).getAllProducts();
    verify(productService,times(1)).getAllProducts();
}

In-Line 4-Line 7, mockMvc performs a GET request to retrieve all products from the URL "/api/v1/products " whose content type is Json. The content is JsonString of product details.

This is the output on running the test.

retrieving all products

Test Case to Retrieve Product by Id

This test uses mockMvc to send a GET request to retrieve a product with a given id.

@Test
public void GetMappingOfProductShouldReturnRespectiveProducct() throws Exception {
    when(productService.getProductByid(product.getId())).thenReturn(product);
    mockMvc.perform(get("/api/v1/product/1").
           contentType(MediaType.APPLICATION_JSON).
           content(asJsonString(product))).
           andExpect(MockMvcResultMatchers.status().isOk()).
           andDo(MockMvcResultHandlers.print());
}

Similarly, in Line 4-Line 8, mockmvc performs a GET request to retrieve a product with given product id 1 from the URL "/api/v1/product/1 " . Its content type is Json and content is JsonString of product details.

The output on running the test is this.

product by id

Test Case to Delete a Product

This test uses mockMvc to send a DELETE request to delete a product with a given id.

@Test
public void DeleteMappingUrlAndIdThenShouldReturnDeletedProduct() throws Exception {
      when(productService.deleteProductById(product.getId())).thenReturn(product);
      mockMvc.perform(delete("/api/v1/product/1")
      .contentType(MediaType.APPLICATION_JSON)
      .content(asJsonString(product)))
      .andExpect(MockMvcResultMatchers.status().isOk()).
      andDo(MockMvcResultHandlers.print());
}

public static String asJsonString(final Object obj){
    try{
        return new ObjectMapper().writeValueAsString(obj);
    }catch (Exception e){
           throw new RuntimeException(e);
      }
}

In Line 4-Line 8, mockmvc performs a DELETE request to delete a product with id 1 from the URL  "/api/v1/product/1 " . The  content type is Json. The content is JsonString of product details.

Now, let’s run the test cases.

The output shows below that all the test cases passed.

test cases for controller

You can find the source code of this post on Github.

For in-depth knowledge of the Spring Framework check my Udemy Best Seller Course Spring Framework 5: Beginner to Guru.

If you’d like to learn more about testing Spring Boot applications with Mockito and JUnit 5, checkout my course Testing Spring Boot: Beginner to Guru.

About SFG Contributor

Staff writer account for Spring Framework Guru

    You May Also Like