Spring Boot Testing using JUnit 5 and Mockito

Introduction

In this chapter, we will explore how to test a Spring Boot application using JUnit 5 and Mockito by building a Product Management project. We will cover setting up the project, creating entities and repositories, writing unit tests for the service layer, and writing integration tests for the controller layer.

Table of Contents

  1. Introduction
  2. Create and Setup Spring Boot Project in IntelliJ IDEA
  3. Configure H2 Database
  4. Create Product Entity
  5. Create Product Repository
  6. Create Service Layer
    • ProductService
    • ProductServiceImpl
  7. Create ProductController
  8. Writing Unit Tests with JUnit 5 and Mockito
  9. Writing Integration Tests with JUnit 5
  10. Conclusion

Create and Setup Spring Boot Project in IntelliJ IDEA

Create a New Spring Boot Project

  1. Open Spring Initializr:

  2. Configure Project Metadata:

    • Project: Maven Project
    • Language: Java
    • Spring Boot: 3.2.0
    • Group: com.example
    • Artifact: product-management
    • Name: product-management
    • Description: Product Management System
    • Package name: com.example.productmanagement
    • Packaging: Jar
    • Java: 17 (or the latest version available)
  3. Add Dependencies:

    • Spring Web
    • Spring Data JPA
    • H2 Database
    • Spring Boot Starter Test
  4. Generate the Project:

    • Click "Generate" to download the project as a ZIP file.
  5. Import Project into IntelliJ IDEA:

    • Open IntelliJ IDEA.
    • Click on "Open" and navigate to the downloaded ZIP file.
    • Extract the ZIP file and import the project.

Explanation

  • Spring Initializr: A web-based tool provided by Spring to bootstrap a new Spring Boot project with dependencies and configurations.
  • Group and Artifact: Define the project’s Maven coordinates.
  • Dependencies: Adding dependencies ensures that the necessary libraries are included in the project for web development, JPA, H2 database connectivity, and testing.

Configure H2 Database

Update application.properties

  1. Open application.properties:

    • Navigate to src/main/resources/application.properties.
  2. Add H2 Database Configuration:

    • Add the following properties to configure the H2 database connection:
spring.datasource.url=jdbc:h2:mem:productdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.hibernate.ddl-auto=update
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

Explanation

  • spring.datasource.url: The JDBC URL to connect to the H2 database in memory.
  • spring.datasource.driverClassName: The driver class name for H2 database.
  • spring.datasource.username: The username to connect to the H2 database.
  • spring.datasource.password: The password to connect to the H2 database.
  • spring.jpa.hibernate.ddl-auto: Specifies the Hibernate DDL mode. Setting it to update automatically updates the database schema based on the entity mappings.
  • spring.h2.console.enabled: Enables the H2 database console for easy access to the database through a web browser.
  • spring.h2.console.path: Specifies the path to access the H2 console.

Create Product Entity

Create the Product Class

  1. Create a New Package:

    • In the src/main/java/com/example/productmanagement directory, create a new package named model.
  2. Create the Product Class:

    • Inside the model package, create a new class named Product.
    • Add the following code to the Product class:
package com.example.productmanagement.model;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String description;
    private double price;

    // Constructors
    public Product() {}

    public Product(String name, String description, double price) {
        this.name = name;
        this.description = description;
        this.price = price;
    }

    // Getters and Setters
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }
}

Explanation

  • @Entity: Specifies that the class is an entity and is mapped to a database table.
  • @Id: Specifies the primary key of the entity.
  • @GeneratedValue: Specifies how the primary key should be generated. GenerationType.IDENTITY indicates that the primary key is auto-incremented.

Create Product Repository

Create the ProductRepository Interface

  1. Create a New Package:

    • In the src/main/java/com/example/productmanagement directory, create a new package named repository.
  2. Create the ProductRepository Interface:

    • Inside the repository package, create a new interface named ProductRepository.
    • Add the following code to the ProductRepository interface:
package com.example.productmanagement.repository;

import com.example.productmanagement.model.Product;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ProductRepository extends JpaRepository<Product, Long> {
}

Explanation

  • JpaRepository: The ProductRepository interface extends JpaRepository, providing CRUD operations for the Product entity. The JpaRepository interface includes methods like save(), findById(), findAll(), deleteById(), etc.
  • Generics: The JpaRepository interface takes two parameters: the entity type (Product) and the type of its primary key (Long).

Create Service Layer

Create ProductService Interface

  1. Create a New Package:

    • In the src/main/java/com/example/productmanagement directory, create a new package named service.
  2. Create the ProductService Interface:

    • Inside the service package, create a new interface named ProductService.
    • Add the following code to the ProductService interface:
package com.example.productmanagement.service;

import com.example.productmanagement.model.Product;
import java.util.List;

public interface ProductService {
    Product saveProduct(Product product);
    Product getProductById(Long id);
    List<Product> getAllProducts();
    Product updateProduct(Long id, Product productDetails);
    void deleteProduct(Long id);
}

Explanation

  • Service Interface: Defines the contract for the service layer. It includes methods for saving, retrieving, updating, and deleting products.

Create ProductServiceImpl Class

  1. Create the ProductServiceImpl Class:
    • Inside the service package, create a new class named ProductServiceImpl.
    • Add the following code to the ProductServiceImpl class:
package com.example.productmanagement.service;

import com.example.productmanagement.model.Product;
import com.example.productmanagement.repository.ProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class ProductServiceImpl implements ProductService {

    @Autowired
    private ProductRepository productRepository;

    @Override
    public Product saveProduct(Product product) {
        return productRepository.save(product);
    }

    @Override
    public Product getProductById(Long id) {
        return productRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("Product not found with id: " + id));
    }

    @Override
    public List<Product> getAllProducts() {
        return productRepository.findAll();
    }

    @Override
    public Product updateProduct(Long id, Product productDetails) {
        Product product = productRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("Product not found with id: " + id));

        product.setName(productDetails.getName());
        product.setDescription(productDetails.getDescription());
        product.setPrice(productDetails.getPrice());

        return productRepository.save(product);
    }

    @Override
    public void deleteProduct(Long id) {
        Product product = productRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("Product not found with id: " + id));

        productRepository.delete(product);
    }
}

Explanation

  • @Service: Indicates that this class is a service component in the Spring context.
  • ProductRepository: The ProductRepository instance is injected into the service class to interact with the database.
  • Exception Handling: The getProductById, updateProduct, and deleteProduct methods throw a RuntimeException if the product is not found.

Create ProductController

Create the ProductController Class

  1. Create a New Package:

    • In the src/main/java/com/example/productmanagement directory, create a new package named controller.
  2. Create the ProductController Class:

    • Inside the controller package, create a new class named ProductController.
    • Add the following code to the ProductController class:
package com.example.productmanagement.controller;

import com.example.productmanagement.model.Product;
import com.example.productmanagement.service.ProductService;
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("/api/products")
public class ProductController {

    @Autowired
    private ProductService productService;

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

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

    @GetMapping
    public ResponseEntity<List<Product>> getAllProducts() {
        List<Product> products = productService.getAllProducts();
        return new ResponseEntity<>(products, HttpStatus.OK);
    }

    @PutMapping("/{id}")
    public ResponseEntity<Product> updateProduct(@PathVariable Long id, @RequestBody Product productDetails) {
        Product updatedProduct = productService.updateProduct(id, productDetails);
        return new ResponseEntity<>(updatedProduct, HttpStatus.OK);
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
        productService.deleteProduct(id);
        return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    }
}

Explanation

  • @RestController: Indicates that this class is a REST controller.
  • @RequestMapping("/api/products"): Maps HTTP requests to /api/products to methods in this controller.
  • @PostMapping: Handles HTTP POST requests to save a product.
  • @GetMapping("/{id}"): Handles HTTP GET requests to retrieve a product by ID.
  • @GetMapping: Handles HTTP GET requests to retrieve all products.
  • @PutMapping("/{id}"): Handles HTTP PUT requests to update a product’s details.
  • @DeleteMapping("/{id}"): Handles HTTP DELETE requests to delete a product by ID.

Writing Unit Tests with JUnit 5 and Mockito

Setup Test Dependencies

  1. Ensure spring-boot-starter-test Dependency:
    • The spring-boot-starter-test dependency should already be included in the pom.xml file. It includes JUnit 5, Mockito, and other testing libraries.

Create Unit Test for ProductService

  1. Create the ProductServiceTest Class:
    • In the src/test/java/com/example/productmanagement/service directory, create a new class named ProductServiceTest.
    • Add the following code to the ProductServiceTest class:
package com.example.productmanagement.service;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

import com.example.productmanagement.model.Product;
import com.example.productmanagement.repository.ProductRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

public class ProductServiceTest {

    @Mock
    private ProductRepository productRepository;

    @InjectMocks
    private ProductServiceImpl productService;

    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);
    }

    @Test
    void testSaveProduct() {
        Product product = new Product("Product1", "Description1", 100.0);
        when(productRepository.save(any(Product.class))).thenReturn(product);

        Product savedProduct = productService.saveProduct(product);

        assertNotNull(savedProduct);
        assertEquals("Product1", savedProduct.getName());
    }

    @Test
    void testGetProductById() {
        Product product = new Product("Product1", "Description1", 100.0);
        when(productRepository.findById(any(Long.class))).thenReturn(Optional.of(product));

        Product foundProduct = productService.getProductById(1L);

        assertNotNull(foundProduct);
        assertEquals("Product1", foundProduct.getName());
    }

    @Test
    void testGetAllProducts() {
        Product product1 = new Product("Product1", "Description1", 100.0);
        Product product2 = new Product("Product2", "Description2", 200.0);
        List<Product> productList = Arrays.asList(product1, product2);
        when(productRepository.findAll()).thenReturn(productList);

        List<Product> products = productService.getAllProducts();

        assertEquals(2, products.size());
    }

    @Test
    void testUpdateProduct() {
        Product existingProduct = new Product("Product1", "Description1", 100.0);
        when(productRepository.findById(any(Long.class))).thenReturn(Optional.of(existingProduct));
        when(productRepository.save(any(Product.class))).thenReturn(existingProduct);

        Product productDetails = new Product("UpdatedProduct", "UpdatedDescription", 150.0);
        Product updatedProduct = productService.updateProduct(1L, productDetails);

        assertNotNull(updatedProduct);
        assertEquals("UpdatedProduct", updatedProduct.getName());
    }

    @Test
    void testDeleteProduct() {
        Product product = new Product("Product1", "Description1", 100.0);
        when(productRepository.findById(any(Long.class))).thenReturn(Optional.of(product));
        doNothing().when(productRepository).delete(any(Product.class));

        assertDoesNotThrow(() -> productService.deleteProduct(1L));
    }
}

Explanation

  • @Mock: Creates a mock implementation of the ProductRepository interface.
  • @InjectMocks: Injects the mock ProductRepository into the ProductServiceImpl instance.
  • MockitoAnnotations.openMocks(this): Initializes the mock objects before each test.
  • Unit Tests: Each test method verifies a specific functionality of the ProductService using JUnit 5 assertions and Mockito stubbing.

Writing Integration Tests with JUnit 5

Create Integration Test for ProductController

  1. Create the ProductControllerIntegrationTest Class:
    • In the src/test/java/com/example/productmanagement/controller directory, create a new class named ProductControllerIntegrationTest.
    • Add the following code to the ProductControllerIntegrationTest class:
package com.example.productmanagement.controller;

import com.example.productmanagement.model.Product;
import com.example.productmanagement.repository.ProductRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;

import java.util.Optional;

import static org.hamcrest.Matchers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@SpringBootTest
@AutoConfigureMockMvc
public class ProductControllerIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ProductRepository productRepository;

    @BeforeEach
    void setUp() {
        productRepository.deleteAll();
    }

    @Test
    void testSaveProduct() throws Exception {
        Product product = new Product("Product1", "Description1", 100.0);
        mockMvc.perform(MockMvcRequestBuilders.post("/api/products")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"name\":\"Product1\", \"description\":\"Description1\", \"price\":100.0}"))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.name").value("Product1"));
    }

    @Test
    void testGetProductById() throws Exception {
        Product product = new Product("Product1", "Description1", 100.0);
        productRepository.save(product);

        mockMvc.perform(MockMvcRequestBuilders.get("/api/products/{id}", product.getId())
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.name").value("Product1"));
    }

    @Test
    void testGetAllProducts() throws Exception {
        Product product1 = new Product("Product1", "Description1", 100.0);
        Product product2 = new Product("Product2", "Description2", 200.0);
        productRepository.save(product1);
        productRepository.save(product2);

        mockMvc.perform(MockMvcRequestBuilders.get("/api/products")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$", hasSize(2)))
                .andExpect(jsonPath("$[0].name").value("Product1"))
                .andExpect(jsonPath("$[1].name").value("Product2"));
    }

    @Test
    void testUpdateProduct() throws Exception {
        Product product = new Product("Product1", "Description1", 100.0);
        productRepository.save(product

);

        mockMvc.perform(MockMvcRequestBuilders.put("/api/products/{id}", product.getId())
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"name\":\"UpdatedProduct\", \"description\":\"UpdatedDescription\", \"price\":150.0}"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.name").value("UpdatedProduct"));
    }

    @Test
    void testDeleteProduct() throws Exception {
        Product product = new Product("Product1", "Description1", 100.0);
        productRepository.save(product);

        mockMvc.perform(MockMvcRequestBuilders.delete("/api/products/{id}", product.getId())
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isNoContent());
    }
}

Explanation

  • @SpringBootTest: Indicates that the class is a Spring Boot test.
  • @AutoConfigureMockMvc: Configures MockMvc for testing the web layer.
  • MockMvc: Used to perform HTTP requests and verify the results.
  • Integration Tests: Each test method performs HTTP requests to the ProductController and verifies the responses using MockMvc assertions.

Conclusion

In this chapter, we built a Product Management System project using Spring Boot. We configured the H2 database, created entities and repositories, wrote unit tests for the service layer using JUnit 5 and Mockito, and wrote integration tests for the controller layer using JUnit 5 and MockMvc. Each step was explained in detail to help you understand how to test a Spring Boot application effectively.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top