The Web Layer is an interface that enables systems to access the business logic of your application.
It is responsible to translate the web-based requests into a format that is used in the core of your system. Also, it transforms the internally used formats into a response that can be sent over the web.
To lower the risk of changing the behavior of your Web Layer by accident, you can use @WebMvcTest
s.
What exactly is a @WebMvcTest?
@WebMvcTest
is one of Spring Boot’s test slices. Integration tests with this annotation will only load a specific subset of beans into the ApplicationContext
. Those beans are all part of the context of the Spring MVC Module. Or as the documentation says:
Using this annotation will disable full auto-configuration and instead apply only configuration relevant to MVC tests (i.e. @Controller, @ControllerAdvice, @JsonComponent, Converter/GenericConverter, Filter, WebMvcConfigurer and HandlerMethodArgumentResolver beans but not @Component, @Service or @Repository beans)
Reducing the scope of the beans can lead to a shorter startup time for your integration tests.
To use @WebMvcTest
, you need to include the dependency org.springframework.boot:spring-boot-starter-test in your project.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
The Example
Before we start coding the @WebMvcTest
, I will give you a quick overview of the classes that will be involved in our test.
The BookController
is a @RestController
and thus part of the Web Layer that we want to test.
@RestController
public class BookController {
private final BookRepository bookRepository;
public BookController(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
@GetMapping(path = "/", produces = MediaType.APPLICATION_JSON_VALUE)
public List<Book> findAll() {
return bookRepository.findAll();
}
@GetMapping(path = "/{isbn}", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Book> findByIsbn(@PathVariable String isbn) {
var book = bookRepository.findByIsbn(isbn)
.orElseThrow(() -> BookNotFoundException.unknownIsbn(isbn));
return ResponseEntity.ok(book);
}
@ResponseStatus(value = HttpStatus.NOT_FOUND)
private static class BookNotFoundException extends RuntimeException {
private BookNotFoundException(String message) {
super(message);
}
public static BookNotFoundException unknownIsbn(String isbn){
return new BookNotFoundException("Can not find book with ISBN '%s'".formatted(isbn));
}
}
}
It exposes two endpoints that can be used to retrieve either all books (findAll
) or one specific book identified by its ISBN (findByIsbn
).
If the ISBN belongs to an unknown book, the controller will throw a BookNotFoundException
which will be converted into a 404 (NOT FOUND) response. The data of the response will always be in JSON format.
The controller depends on a BookRepository
which loads data from a data store.
To keep things simple, we won’t use a database but a simple List<Book>
.
@Repository
public class InMemoryBookRepository implements BookRepository{
private List<Book> books = List.of(new Book("Spring Boot 2", "Michael Simons", "978-3-86490-525-4")
, new Book("Langlebige Software-Architekturen", "Carola Lilienthal", "978-3-86490-729-6"));
@Override
public List<Book> findAll() {
return Collections.unmodifiableList(books);
}
@Override
public Optional<Book> findByIsbn(String isbn) {
return books.stream().filter(book -> book.getIsbn().equals(isbn)).findFirst();
}
}
Test Setup
The following listing shows the basic structure of the test class.
@WebMvcTest({BookController.class})
class BookControllerTest {
@MockBean
private BookRepository bookRepository;
@Autowired
private MockMvc mockMvc;
// Tests
}
The first step of the setup is the @WebMvcTest
annotation on the test class itself. As mentioned above, it ensures that only beans relevant to the Web layer are loaded into the ApplicationContext
. You can reduce this scope even more by passing specific controllers as value into the annotation. In this example, the BookController
is the only controller that will be loaded.
The @MockBean
annotation adds a mock into the ApplicationContext
. This is required to provide a BookRepository
bean that can be injected into the BookController
. Otherwise, the test could not start and would fail. The real implementation – the InMemoryBookRepository
– is not loaded because of its @Repository
annotation.
The last part of the setup code is the MockMvc
. It is part of Spring’s testing tools and helps you to send requests and make assertions on the response.
Testing for the correct status code
With the basic setup done, we can now start implementing the first test. One part of the behavior of our controller is to return the correct HTTP status code. If the request succeeded, the server responds with a 200 (OK) status code. If the requested resource could not be found, it responds with a 404 NOT FOUND.
@Test
void returnsListOfBooks() throws Exception {
mockMvc.perform(get("/"))
.andExpect(status().isOk());
}
We use MockMvc
to perform a GET-Request against “/” and expect to receive a response with the status code 200 (OK).
The fluent API of MockMvc
allows us to describe our test in a way we would do with a natural language. With the help of the autocompletion of your IDE and a bit of practice, it becomes a breeze to test your Web Layer.
Testing for the status code 404 (NOT FOUND) needs a bit of preparation.
To make sure that it’s impossible to find an existing book by accident we use our mocked BookRepository
.
@Test
void returns404_WhenThereIsNoBookWithTheGivenIsbn() throws Exception {
doReturn(Optional.empty()).when(bookRepository).findByIsbn(anyString());
mockMvc.perform(get("/123-4-56789-123-3"))
.andExpect(status().isNotFound());
}
With the first line of the test, we mock the behavior of the BookRepository
. Every time findByIsbn
is called with any string, the mock will return an empty Optional
. The other part of the test is nearly identical to the previous one. We use MockMvc
to perform a GET request and test for the correct status code.
Testing for the correct content
In the previous tests, we made assertions about the status code of the responses. In this example, we will extend the first test case to test the arguably more important part – the content itself. Like before we use MockMvc
for our assertions.
@Test
void returnsListOfBooks() throws Exception {
var books = List.of(new Book("Spring Boot 2", "Michael Simons", "978-3-86490-525-4")
, new Book("Langlebige Software-Architekturen", "Carola Lilienthal", "978-3-86490-729-6"));
doReturn(books).when(bookRepository).findAll();
mockMvc.perform(get("/"))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$", hasSize(2)))
.andExpect(jsonPath("$.[0].title", is(books.get(0).getTitle())))
.andExpect(jsonPath("$.[0].author", is(books.get(0).getAuthor())))
.andExpect(jsonPath("$.[0].isbn", is(books.get(0).getIsbn())));
}
The first lines are used to mock the behavior of the BookRepository
.
Every time its findAll
method is called, it will return a hardcoded list of books.
The new assertions can be found in lines 10-14. We can use MockMvc::andExpect
in combination with the jsonPath
method to test the structure and the content of our response. The first parameter of jsonPath
– the expression
– is a JSONPath. It is used to address a specific part of a JSON structure.
If you want to learn more about the syntax of JSONPath, I recommend having a look at this article.
Conclusion
Spring Boot offers a great toolset when it comes to testing your application. You can use MockMvc
to call the endpoints of your controllers and make assertions about the response. To support you with the assertions, Spring Boot provides a collection of predefined ResultMatchers
that can be accessed via org.springframework.test.web.servlet.result.MockMvcResultMatchers
.
By using @WebMvcTest
only beans relevant to the Web Layer will be loaded into the ApplicationContext
. This can reduce the execution time of your tests, which will lead to faster feedback in the development process.
Resources
- you can find the code of this blog post in this GitHub repository
- have a look at Spring Boot’s documentation for more official information about
@WebMvcTest
- for more information about JSONPath have a look at this article