Spring Boot – Rest Controller Unit Test with @WebMvcTest

Nowadays Unit Test is so important in Software Development, and Spring Boot Test also provides @WebMvcTest annotation to make writing unit test for Rest Controller more simpler. In this tutorial, we’re gonna look at how to apply @WebMvcTest in our Spring Boot Project with JUnit 5 and Mockito.

More Practice:
@DataJpaTest example in Spring Boot
Validate Request Body in Spring Boot
@RestControllerAdvice example in Spring Boot
Spring Boot Token based Authentication with Spring Security example
– Documentation: Spring Boot + Swagger 3 example (with OpenAPI 3)
– Caching: Spring Boot Redis Cache example

Associations:
Spring Boot One To One example with JPA, Hibernate
Spring Boot One To Many example with JPA, Hibernate
Spring Boot Many to Many example with JPA, Hibernate


Spring Boot test Rest Controller Overview

We’ve created Rest Controller for CRUD Operations and finder method.
Let look at the code:
(step by step to build the Rest APIs is in:
Spring Boot + H2
Spring Boot + MySQL
Spring Boot + PostgreSQL
Spring Data + MongoDB
Spring JPA + SQL Server
Spring JPA + Oracle
Spring Data + Cassandra)

@RestController
public class TutorialController {

  @Autowired
  TutorialRepository tutorialRepository;

  @GetMapping("/tutorials")
  public ResponseEntity<List<Tutorial>> getAllTutorials(@RequestParam(required = false) String title) {
    try {
      ...
      return new ResponseEntity<>(tutorials, HttpStatus.OK);
    } catch (Exception e) {
      return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
    }
  }
  
  @GetMapping("/tutorials/{id}")
  public ResponseEntity<Tutorial> getTutorialById(@PathVariable("id") long id) {
    Optional<Tutorial> tutorialData = tutorialRepository.findById(id);

    if (tutorialData.isPresent()) {
      return new ResponseEntity<>(tutorialData.get(), HttpStatus.OK);
    } else {
      return new ResponseEntity<>(HttpStatus.NOT_FOUND);
    }
  }
  
  @PutMapping("/tutorials/{id}")
  public ResponseEntity<Tutorial> updateTutorial(@PathVariable("id") long id, @RequestBody Tutorial tutorial) {
    Optional<Tutorial> tutorialData = tutorialRepository.findById(id);

    if (tutorialData.isPresent()) {
      ...
      return new ResponseEntity<>(tutorialRepository.save(_tutorial), HttpStatus.OK);
    } else {
      return new ResponseEntity<>(HttpStatus.NOT_FOUND);
    }
  }

  ...

  @DeleteMapping("/tutorials/{id}")
  public ResponseEntity<HttpStatus> deleteTutorial(@PathVariable("id") long id) {
    try {
      tutorialRepository.deleteById(id);
      return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    } catch (Exception e) {
      return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
    }
  }

  @DeleteMapping("/tutorials")
  public ResponseEntity<HttpStatus> deleteAllTutorials() {
    // try and catch
  }
}

Is there any way to test Rest Controller in Spring Boot? Any way to:
– know that an HTTP request is mapped to correct endpoints (with @GetMapping, @PostMapping, @PutMapping and @DeleteMapping)
– know that path variables are mapped with @PathVariable
– know that the @RequestBody and @ResponseBody work correctly

Let’s write the test cases with @WebMvcTest and Mockito, then run Spring Boot test Rest Controller with JUnit 5.

spring boot rest api unit testing with junit 5

For testing, we’ll work with H2 in-memory database. It eliminates the need for configuring and starting an actual database.

Spring Boot @WebMvcTest

Spring Boot @WebMvcTest annotation provides simple way to test Rest Controller, it disables full auto-configuration (@Component, @Service or @Repository beans will not be scanned) and apply only configuration relevant to the web layer (@Controller, @ControllerAdvice, @JsonComponent, WebMvcConfigurer beans…).

If you have multiple controllers, you can make only one instantiate by using @WebMvcTest(TutorialController.class).

@WebMvcTest(TutorialController.class)
public class TutorialControllerTests {

}

Now look at our Rest Controller (TutorialController) which has a dependency component TutorialRepository:

@RestController
public class TutorialController {

  @Autowired
  TutorialRepository tutorialRepository;

  ...
}

Spring automatically injects the dependency into the controller.

Let’s think about the test class.

– We need to use @MockBean annotation to create and inject a mock of the TutorialRepository. It helps application context start), then we will set the expectations using Mockito.

– We need to fake HTTP requests. So we will autowire a MockMvc bean which Spring Boot autoconfigures.

@WebMvcTest(TutorialController.class)
public class TutorialControllerTests {
  @MockBean
  private TutorialRepository tutorialRepository;

  @Autowired
  private MockMvc mockMvc;

  @Test
  void shouldDoSomething() throws Exception {
    // set expectation for TutorialRepository using Mockito
    when(tutorialRepository...).thenReturn(...);

    // perform HTTP request and set the expectations with MockMVC
    mockMvc.perform(...).andExpect(...).andExpect(...);
  }
}

Spring Boot test Rest Controller example

Technology

  • Spring Boot 2
  • Mockito 4
  • Junit 5
  • Maven 3.8.5
  • H2 database

Project Structure

spring-boot-test-rest-controller-project-structure

Tutorial data model class corresponds to entity and table tutorials.
TutorialRepository handles CRUD methods and custom finder methods. It will be autowired in TutorialController and mocked in TutorialControllerTests.
TutorialControllerTests is the main Test Class used for testing Rest Controller and annotated with @WebMvcTest.
– pom.xml contains dependencies for Spring Boot Test, Web, Spring Data, H2 database.

Setup Spring Boot Test Rest Controller Project

This tutorial gives you an additional unit test for following Rest APIs example:
Spring Boot + H2
Spring Boot + MySQL
Spring Boot + PostgreSQL
Spring Data + MongoDB
Spring JPA + SQL Server
Spring JPA + Oracle
Spring Data + Cassandra

So you only need to create a new file named TutorialControllerTests.java in src/test/java/[package].

This is a typical pom.xml with dependencies:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-test</artifactId>
	<scope>test</scope>
</dependency>

<dependency>
	<groupId>com.h2database</groupId>
	<artifactId>h2</artifactId>
	<scope>test</scope>
</dependency>

Spring Boot @WebMvcTest example

Let’s apply @WebMvcTest in our Spring Boot Rest API Unit Test with JUnit 5 and Mockito.

In src/test/java/com.bezkoder.spring.test, create a file with name: TutorialControllerTests.java.

Inside the file, we will define TutorialControllerTests to unit test the Rest API endpoints which has following methods:

  • shouldCreateTutorial(): POST /api/tutorials
  • shouldReturnTutorial(): GET /api/tutorials/[:id]
  • shouldReturnNotFoundTutorial(): GET /api/tutorials/[:id] – 404
  • shouldReturnListOfTutorials(): GET /api/tutorials
  • shouldReturnListOfTutorialsWithFilter(): GET /api/tutorials?title=[:title]
  • shouldReturnNoContentWhenFilter(): GET /api/tutorials?title=[:title] – 204
  • shouldUpdateTutorial(): PUT /api/tutorials/[:id]
  • shouldReturnNotFoundUpdateTutorial(): PUT /api/tutorials/[:id] – 404
  • shouldDeleteTutorial(): DELETE /api/tutorials/[:id]
  • shouldDeleteAllTutorials(): DELETE /api/tutorials
package com.bezkoder.spring.test;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

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

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;

import com.bezkoder.spring.test.controller.TutorialController;
import com.bezkoder.spring.test.model.Tutorial;
import com.bezkoder.spring.test.repository.TutorialRepository;
import com.fasterxml.jackson.databind.ObjectMapper;

@WebMvcTest(TutorialController.class)
public class TutorialControllerTests {
  @MockBean
  private TutorialRepository tutorialRepository;

  @Autowired
  private MockMvc mockMvc;

  @Autowired
  private ObjectMapper objectMapper;

  @Test
  void shouldCreateTutorial() throws Exception {
    Tutorial tutorial = new Tutorial(1, "Spring Boot @WebMvcTest", "Description", true);

    mockMvc.perform(post("/api/tutorials").contentType(MediaType.APPLICATION_JSON)
        .content(objectMapper.writeValueAsString(tutorial)))
        .andExpect(status().isCreated())
        .andDo(print());
  }

  @Test
  void shouldReturnTutorial() throws Exception {
    long id = 1L;
    Tutorial tutorial = new Tutorial(id, "Spring Boot @WebMvcTest", "Description", true);

    when(tutorialRepository.findById(id)).thenReturn(Optional.of(tutorial));
    mockMvc.perform(get("/api/tutorials/{id}", id)).andExpect(status().isOk())
        .andExpect(jsonPath("$.id").value(id))
        .andExpect(jsonPath("$.title").value(tutorial.getTitle()))
        .andExpect(jsonPath("$.description").value(tutorial.getDescription()))
        .andExpect(jsonPath("$.published").value(tutorial.isPublished()))
        .andDo(print());
  }

  @Test
  void shouldReturnNotFoundTutorial() throws Exception {
    long id = 1L;

    when(tutorialRepository.findById(id)).thenReturn(Optional.empty());
    mockMvc.perform(get("/api/tutorials/{id}", id))
         .andExpect(status().isNotFound())
         .andDo(print());
  }

  @Test
  void shouldReturnListOfTutorials() throws Exception {
    List<Tutorial> tutorials = new ArrayList<>(
        Arrays.asList(new Tutorial(1, "Spring Boot @WebMvcTest 1", "Description 1", true),
            new Tutorial(2, "Spring Boot @WebMvcTest 2", "Description 2", true),
            new Tutorial(3, "Spring Boot @WebMvcTest 3", "Description 3", true)));

    when(tutorialRepository.findAll()).thenReturn(tutorials);
    mockMvc.perform(get("/api/tutorials"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.size()").value(tutorials.size()))
        .andDo(print());
  }

  @Test
  void shouldReturnListOfTutorialsWithFilter() throws Exception {
    List<Tutorial> tutorials = new ArrayList<>(
        Arrays.asList(new Tutorial(1, "Spring Boot @WebMvcTest", "Description 1", true),
            new Tutorial(3, "Spring Boot Web MVC", "Description 3", true)));

    String title = "Boot";
    MultiValueMap<String, String> paramsMap = new LinkedMultiValueMap<>();
    paramsMap.add("title", title);

    when(tutorialRepository.findByTitleContaining(title)).thenReturn(tutorials);
    mockMvc.perform(get("/api/tutorials").params(paramsMap))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.size()").value(tutorials.size()))
        .andDo(print());

    tutorials = Collections.emptyList();

    when(tutorialRepository.findByTitleContaining(title)).thenReturn(tutorials);
    mockMvc.perform(get("/api/tutorials").params(paramsMap))
        .andExpect(status().isNoContent())
        .andDo(print());
  }
  
  @Test
  void shouldReturnNoContentWhenFilter() throws Exception {
    String title = "BezKoder";
    MultiValueMap<String, String> paramsMap = new LinkedMultiValueMap<>();
    paramsMap.add("title", title);
    
    List<Tutorial> tutorials = Collections.emptyList();

    when(tutorialRepository.findByTitleContaining(title)).thenReturn(tutorials);
    mockMvc.perform(get("/api/tutorials").params(paramsMap))
        .andExpect(status().isNoContent())
        .andDo(print());
  }

  @Test
  void shouldUpdateTutorial() throws Exception {
    long id = 1L;

    Tutorial tutorial = new Tutorial(id, "Spring Boot @WebMvcTest", "Description", false);
    Tutorial updatedtutorial = new Tutorial(id, "Updated", "Updated", true);

    when(tutorialRepository.findById(id)).thenReturn(Optional.of(tutorial));
    when(tutorialRepository.save(any(Tutorial.class))).thenReturn(updatedtutorial);

    mockMvc.perform(put("/api/tutorials/{id}", id).contentType(MediaType.APPLICATION_JSON)
        .content(objectMapper.writeValueAsString(updatedtutorial)))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.title").value(updatedtutorial.getTitle()))
        .andExpect(jsonPath("$.description").value(updatedtutorial.getDescription()))
        .andExpect(jsonPath("$.published").value(updatedtutorial.isPublished()))
        .andDo(print());
  }
  
  @Test
  void shouldReturnNotFoundUpdateTutorial() throws Exception {
    long id = 1L;

    Tutorial updatedtutorial = new Tutorial(id, "Updated", "Updated", true);

    when(tutorialRepository.findById(id)).thenReturn(Optional.empty());
    when(tutorialRepository.save(any(Tutorial.class))).thenReturn(updatedtutorial);

    mockMvc.perform(put("/api/tutorials/{id}", id).contentType(MediaType.APPLICATION_JSON)
        .content(objectMapper.writeValueAsString(updatedtutorial)))
        .andExpect(status().isNotFound())
        .andDo(print());
  }
  
  @Test
  void shouldDeleteTutorial() throws Exception {
    long id = 1L;

    doNothing().when(tutorialRepository).deleteById(id);
    mockMvc.perform(delete("/api/tutorials/{id}", id))
         .andExpect(status().isNoContent())
         .andDo(print());
  }
  
  @Test
  void shouldDeleteAllTutorials() throws Exception {
    doNothing().when(tutorialRepository).deleteAll();
    mockMvc.perform(delete("/api/tutorials"))
         .andExpect(status().isNoContent())
         .andDo(print());
  }
}

Run Spring Boot Rest API unit testing with Junit 5

– First run command: mvn clean install.
– Then run Test: mvn test -Dtest=TutorialControllerTests

Here are several examples and the result:

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /api/tutorials/1
       Parameters = {}
          Headers = []
             Body = null
    Session Attrs = {}

Handler:
             Type = com.bezkoder.spring.test.controller.TutorialController
           Method = com.bezkoder.spring.test.controller.TutorialController#getTutorialById(long)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Vary:"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers", Content-Type:"application/json"]
     Content type = application/json
             Body = {"id":1,"title":"Spring Boot @WebMvcTest","description":"Description","published":true}
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

[... ... ...]

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /api/tutorials
       Parameters = {title=[Boot]}
          Headers = []
             Body = null
    Session Attrs = {}

Handler:
             Type = com.bezkoder.spring.test.controller.TutorialController
           Method = com.bezkoder.spring.test.controller.TutorialController#getAllTutorials(String)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Vary:"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers", Content-Type:"application/json"]
     Content type = application/json
             Body = [{"id":1,"title":"Spring Boot @WebMvcTest","description":"Description 1","published":true},{"id":3,"title":"Spring Boot Web MVC","description":"Description 3","published":true}]
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

[... ... ...]

MockHttpServletRequest:
      HTTP Method = PUT
      Request URI = /api/tutorials/1
       Parameters = {}
          Headers = [Content-Type:"application/json;charset=UTF-8", Content-Length:"67"]
             Body = {"id":1,"title":"Updated","description":"Updated","published":true}
    Session Attrs = {}

Handler:
             Type = com.bezkoder.spring.test.controller.TutorialController
           Method = com.bezkoder.spring.test.controller.TutorialController#updateTutorial(long, Tutorial)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Vary:"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers", Content-Type:"application/json"]
     Content type = application/json
             Body = {"id":1,"title":"Updated","description":"Updated","published":true}
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

MockHttpServletRequest:
      HTTP Method = POST
      Request URI = /api/tutorials
       Parameters = {}
          Headers = [Content-Type:"application/json;charset=UTF-8", Content-Length:"87"]
             Body = {"id":1,"title":"Spring Boot @WebMvcTest","description":"Description","published":true}
    Session Attrs = {}

Handler:
             Type = com.bezkoder.spring.test.controller.TutorialController
           Method = com.bezkoder.spring.test.controller.TutorialController#createTutorial(Tutorial)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 201
    Error message = null
          Headers = [Vary:"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers"]
     Content type = null
             Body =
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

[... ... ...]

[INFO] Tests run: 10, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.436 s - in com.bezkoder.spring.test.TutorialControllerTests
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 10, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  5.151 s
[INFO] Finished at: 2022-03-28T10:15:22+07:00
[INFO] ------------------------------------------------------------------------

We have another way to run Junit 5 test in Spring Tool Suite with UI.
In Package Explorer, Right Click on TutorialControllerTests.java -> Run as -> JUnit Test:

spring boot rest api unit testing with junit 5

Conclusion

Today we’ve create Spring Boot Test for Rest Controller with Junit 5 using @WebMvcTest and Mockito. We also run unit test for many CRUD and finder methods endpoints.

You may need to handle Exception with:
Spring Boot @ControllerAdvice & @ExceptionHandler example

Happy learning! See you again.

Further Reading

More Practice:
@DataJpaTest example in Spring Boot
@RestControllerAdvice example in Spring Boot
Spring Boot Token based Authentication with Spring Security example
Validate Request Body in Spring Boot

Associations:
Spring Boot One To One example with JPA, Hibernate
Spring Boot One To Many example with JPA, Hibernate
Spring Boot Many to Many example with JPA, Hibernate

Source Code

You can find the complete source code for this tutorial on Github.

The code gives you an additional unit test for following Rest APIs example:
Spring Boot + H2
Spring Boot + MySQL
Spring Boot + PostgreSQL
Spring Data + MongoDB
Spring JPA + SQL Server
Spring JPA + Oracle
Spring Data + Cassandra

Documentation: Spring Boot + Swagger 3 example (with OpenAPI 3)
Caching: Spring Boot Redis Cache example