Тестирование Spring Boot через MockMVC

Автор статьи: Рустем Галиев
IBM Senior DevOps Engineer & Integration Architect. Официальный DevOps ментор и коуч в IBM
Привет, Хабр! Сегодня мы посмотрим на то, как тестировать Spring Boot через MockMVC.
MockMvc – это тестовый фреймворк на стороне сервера, который позволяет проверять большинство функциональных возможностей приложения Spring MVC с помощью облегченных и целевых тестов
Прежде чем мы изучим механизм написания тестов с помощью MockMVC, нам нужно понять, почему вы хотите это сделать. Берем код простого книжного приложения. Давайте начнем с просмотра этого кода.
BookController.java
package books;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/books")
public class BookController {
private final BookService bookService;
public BookController(BookService bookService) {
this.bookService = bookService;
}
@GetMapping
public List<Book> findAll() {
return bookService.findAll();
}
@GetMapping("/{id}")
public Book findOne(@PathVariable int id) {
return bookService.findOne(id);
}
}
Класс помечен @RestController, что означает, что это класс, который будет принимать запросы и возвращать ответы:
Следующий код BookService.java
package books;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
@Service
public class BookService {
private List<Book> books = new ArrayList<>();
public List<Book> findAll() {
return books;
}
public Book findOne(int id) {
return books.stream().filter(book -> book.getId() == id).findFirst().orElseThrow(BookNotFoundException::new);
}
/**
* This method will be called once after the bean was initialized and add some seed data to the books list.
*/
@PostConstruct
private void loadBooks() {
Book one = new Book(1,
"97 Things Every Java Programmer Should Know",
"Kevlin Henney, Trisha Gee",
"OReilly Media, Inc.",
"May 2020",
"9781491952696",
"Java");
Book two = new Book(2,
"Spring Boot: Up and Running",
"Mark Heckler",
"OReilly Media, Inc.",
"February 2021",
"9781492076919",
"Spring");
Book three = new Book(3,
"Hacking with Spring Boot 2.3: Reactive Edition",
"Greg L. Turnquist",
"Amazon.com Services LLC",
"May 2020",
"B086722L4L",
"Spring");
books.addAll(Arrays.asList(one,two,three));
}
}
Экземпляр класса BookService
автоматически подключается Spring с помощью внедрения конструктора. Файл создает три книги и сохраняет их в списке. Метод findAll()
вернет все книги в коллекции, а метод findOne
вернет одну книгу или выдаст исключение, если она не найдена.
Прежде чем вносить какие-либо изменения, нам, вероятно, следует запустить приложение, чтобы убедиться, что все работает. Запустим приложение:mvn spring-boot:run
Мы смогли запустить приложение и вручную протестировать каждую из конечных точек. Но что, если конечных точек больше двух? А если их пятьсот? Ручное тестирование каждого из них потребует много времени и чревато ошибками. Мы также хотим, чтобы ваши тесты выполнялись автоматически в процессе CI/CD, чтобы мы могли убедиться, что приложение работает должным образом, прежде чем развертывать его в другой среде.
Теперь, когда вы знаете, зачем вам нужно писать тесты, давайте посмотрим, как использовать MockMvc.
Создадим файл BookControllerTest.java
package books;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
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.test.web.servlet.MockMvc;
import java.util.List;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
// web-mvc-test
class BookControllerTest {
// mock-mvc
// mock-bean
// test-find-all
// test-find-one
private List<Book> getBooks() {
Book one = new Book(1,
"97 Things Every Java Programmer Should Know",
"Kevlin Henney, Trisha Gee",
"OReilly Media, Inc.",
"May 2020",
"9781491952696",
"Java");
Book two = new Book(2,
"Spring Boot: Up and Running",
"Mark Heckler",
"OReilly Media, Inc.",
"February 2021",
"9781492076919",
"Spring");
return List.of(one, two);
}
}
Затем мы добавляем следующую аннотацию над объявлением класса:
@WebMvcTest(BookController.class)
Аннотацию WebMvcTest
можно использовать для теста Spring MVC, ориентированного только на компоненты Spring MVC. Использование этой аннотации отключит полную автоконфигурацию и вместо этого применит только конфигурацию, относящуюся к тестам MVC (например, @Controller, @ControllerAdvice, @JsonComponent, Converter/GenericConverter, Filter, WebMvcConfigurer и HandlerMethodArgumentResolver bean-компоненты, но не @Component, @Service или @Repository бобы).
В этом случае мы сообщаем Spring, что единственный bean-компонент, который следует использовать в этом тесте, — это класс BookController
. Это может не иметь большого значения в нашем небольшом примере приложения, но по мере того, как наши приложения растут, и у вас есть сотни или тысячи bean-компонентов, управляемых Spring, мы не хотим, чтобы они все загружались в ApplicationContext
только для запуска этого единственного теста.
По умолчанию тесты, аннотированные @WebMvcTest, также автоматически настраивают Spring Security и MockMVC. Это означает, что просто используя эту аннотацию, мы получим доступ к экземпляру MockMVC.
Инфраструктура Spring MVC Test, также известная как MockMVC, обеспечивает поддержку тестирования приложений Spring MVC. Он выполняет полную обработку запросов Spring MVC, но через фиктивные объекты запросов и ответов вместо работающего сервера.
На предыдущем шаге вы добавили аннотацию @WebMvcTest в класс BookControllerTest. При этом Spring автоматически настроит MockMVC для нас, и мы сможем получить экземпляр, добавив следующий код:
@Autowired
MockMvc mvc;
Когда мы пишем юнит тесты, вы хотите сосредоточиться на одной функциональной единице. В этом случае вы хотите протестировать класс BookController
. Если вы помните наш обзор приложения, конструктор BookController
отвечает за подключение BookService
. Мы можем использовать аннотацию Mockito @MockBean, чтобы добавить макеты в контекст приложения Spring. Затем в наших отдельных тестах мы можем имитировать поведение методов BookService:
@MockBean
BookService bookService;
Теперь, когда у нас есть вся инфраструктура, мы можем приступить к написанию тестов нашего контроллера. Первое, что мы напишем, — это тестирование метода BookController.findAll()
. Мы добавляем следующий код в ваш тестовый класс:
@Test
void findAllShouldReturnAllBooks() throws Exception {
Mockito.when(this.bookService.findAll()).thenReturn(getBooks());
mvc.perform(get("/books"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(2));
}
Метод Mockito.when() имитирует поведение метода findAll() BookService. Когда этот метод встречается в контроллере, он будет использовать ваше фиктивное поведение для возврата списка книг.
Метод execute исходит от MockMvc и выполняет запрос и возвращает тип, который позволяет связывать дальнейшие действия, такие как утверждение ожиданий, с результатом. Метод andExpect подтверждает ожидаемый результат. В этом примере мы проверяем успешный ответ, содержащий ожидаемое количество книг.
Если приложение уже запущено с предыдущего шага, вам нужно будет остановить его из командной строки. Затем запустите приложение с помощью следующей команды:mvn spring-boot:run
Запустим тест
mvn -Dtest=BookControllerTest#findAllShouldReturnAllBooks test
Далее нам нужно протестировать функциональность получения одной книги. Мы будем использовать MockMVC для отправки запроса GET к /books/1, который является действительной книгой и должен вернуть первую книгу в коллекции. Оттуда мы проверяем значения в книге:
@Test
void findOneShouldReturnValidBook() throws Exception {
Mockito.when(this.bookService.findOne(1)).thenReturn(getBooks().get(0));
mvc.perform(get("/books/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.title").value("97 Things Every Java Programmer Should Know"))
.andExpect(jsonPath("$.author").value("Kevlin Henney, Trisha Gee"))
.andExpect(jsonPath("$.publisher").value("OReilly Media, Inc."))
.andExpect(jsonPath("$.releaseDate").value("May 2020"))
.andExpect(jsonPath("$.isbn").value("9781491952696"))
.andExpect(jsonPath("$.topic").value("Java"));
}
Запустим тестmvn -Dtest=BookControllerTest#findOneShouldReturnValidBook test
Мы выполнили команду для запуска одного теста. Если вы хотите запустить все тесты, вы можете сделать это, выполнив следующую команду:mvn test
Полный код:
package books;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
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.test.web.servlet.MockMvc;
import java.util.List;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(BookController.class)
class BookControllerTest {
@Autowired
MockMvc mvc;
@MockBean
BookService bookService;
@Test
void findAllShouldReturnAllBooks() throws Exception {
Mockito.when(this.bookService.findAll()).thenReturn(getBooks());
mvc.perform(get("/books"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(2));
}
@Test
void findOneShouldReturnValidBook() throws Exception {
Mockito.when(this.bookService.findOne(1)).thenReturn(getBooks().get(0));
mvc.perform(get("/books/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.title").value("97 Things Every Java Programmer Should Know"))
.andExpect(jsonPath("$.author").value("Kevlin Henney, Trisha Gee"))
.andExpect(jsonPath("$.publisher").value("OReilly Media, Inc."))
.andExpect(jsonPath("$.releaseDate").value("May 2020"))
.andExpect(jsonPath("$.isbn").value("9781491952696"))
.andExpect(jsonPath("$.topic").value("Java"));
}
private List<Book> getBooks() {
Book one = new Book(1,
"97 Things Every Java Programmer Should Know",
"Kevlin Henney, Trisha Gee",
"OReilly Media, Inc.",
"May 2020",
"9781491952696",
"Java");
Book two = new Book(2,
"Spring Boot: Up and Running",
"Mark Heckler",
"OReilly Media, Inc.",
"February 2021",
"9781492076919",
"Spring");
return List.of(one, two);
}
}
В заключение приглашаем всех желающих на открытое занятие «Введение в облака, создание кластера в Mongo DB Atlas». На нем поговорим, какие бывают облака и настроим бесплатный Mongo DB кластер для своих проектов. Записаться на урок можно на странице курса «Разработчик на Spring Framework».