Testing Spring Boot RESTful Services
Last Updated on January 14, 2021 by jt
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.
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.
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.
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.
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.
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.





