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.