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
- Introduction
- Create and Setup Spring Boot Project in IntelliJ IDEA
- Configure H2 Database
- Create Product Entity
- Create Product Repository
- Create Service Layer
- ProductService
- ProductServiceImpl
- Create ProductController
- Writing Unit Tests with JUnit 5 and Mockito
- Writing Integration Tests with JUnit 5
- Conclusion
Create and Setup Spring Boot Project in IntelliJ IDEA
Create a New Spring Boot Project
-
Open Spring Initializr:
- Go to Spring Initializr in your web browser.
-
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)
-
Add Dependencies:
- Spring Web
- Spring Data JPA
- H2 Database
- Spring Boot Starter Test
-
Generate the Project:
- Click "Generate" to download the project as a ZIP file.
-
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
-
Open
application.properties:- Navigate to
src/main/resources/application.properties.
- Navigate to
-
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 toupdateautomatically 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
-
Create a New Package:
- In the
src/main/java/com/example/productmanagementdirectory, create a new package namedmodel.
- In the
-
Create the
ProductClass:- Inside the
modelpackage, create a new class namedProduct. - Add the following code to the
Productclass:
- Inside the
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.IDENTITYindicates that the primary key is auto-incremented.
Create Product Repository
Create the ProductRepository Interface
-
Create a New Package:
- In the
src/main/java/com/example/productmanagementdirectory, create a new package namedrepository.
- In the
-
Create the
ProductRepositoryInterface:- Inside the
repositorypackage, create a new interface namedProductRepository. - Add the following code to the
ProductRepositoryinterface:
- Inside the
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: TheProductRepositoryinterface extendsJpaRepository, providing CRUD operations for theProductentity. TheJpaRepositoryinterface includes methods likesave(),findById(),findAll(),deleteById(), etc.- Generics: The
JpaRepositoryinterface takes two parameters: the entity type (Product) and the type of its primary key (Long).
Create Service Layer
Create ProductService Interface
-
Create a New Package:
- In the
src/main/java/com/example/productmanagementdirectory, create a new package namedservice.
- In the
-
Create the
ProductServiceInterface:- Inside the
servicepackage, create a new interface namedProductService. - Add the following code to the
ProductServiceinterface:
- Inside the
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
- Create the
ProductServiceImplClass:- Inside the
servicepackage, create a new class namedProductServiceImpl. - Add the following code to the
ProductServiceImplclass:
- Inside the
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: TheProductRepositoryinstance is injected into the service class to interact with the database.- Exception Handling: The
getProductById,updateProduct, anddeleteProductmethods throw aRuntimeExceptionif the product is not found.
Create ProductController
Create the ProductController Class
-
Create a New Package:
- In the
src/main/java/com/example/productmanagementdirectory, create a new package namedcontroller.
- In the
-
Create the
ProductControllerClass:- Inside the
controllerpackage, create a new class namedProductController. - Add the following code to the
ProductControllerclass:
- Inside the
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/productsto 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
- Ensure
spring-boot-starter-testDependency:- The
spring-boot-starter-testdependency should already be included in thepom.xmlfile. It includes JUnit 5, Mockito, and other testing libraries.
- The
Create Unit Test for ProductService
- Create the
ProductServiceTestClass:- In the
src/test/java/com/example/productmanagement/servicedirectory, create a new class namedProductServiceTest. - Add the following code to the
ProductServiceTestclass:
- In the
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 theProductRepositoryinterface.@InjectMocks: Injects the mockProductRepositoryinto theProductServiceImplinstance.MockitoAnnotations.openMocks(this): Initializes the mock objects before each test.- Unit Tests: Each test method verifies a specific functionality of the
ProductServiceusing JUnit 5 assertions and Mockito stubbing.
Writing Integration Tests with JUnit 5
Create Integration Test for ProductController
- Create the
ProductControllerIntegrationTestClass:- In the
src/test/java/com/example/productmanagement/controllerdirectory, create a new class namedProductControllerIntegrationTest. - Add the following code to the
ProductControllerIntegrationTestclass:
- In the
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
ProductControllerand 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.