Mock MVC#
Данный модуль:
является дополнением к стандартному Spring MockMvc
данный модуль предоставляет фасад , который фиксирует работу Spring MockMvc в виде шагов
данный модуль исправляет потенциальные неудобства флоу Spring MockMvc
<dependency>
<!--необходимо иметь в classpath-->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<!--диапазон поддерживаемых версий-->
<version>[2.6.1,)</version>
</dependency>
<dependency>
<!--необходимо иметь в test classpath-->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<!--диапазон поддерживаемых версий-->
<version>[2.6.1,)</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ru.tinkoff.qa.neptune</groupId>
<artifactId>spring.mock.mvc</artifactId>
<version>${LATEST_RELEASE_OR_BETA_VERSION}</version>
<scope>test</scope>
</dependency>
dependencies {
//необходимо иметь в classpath
implementation group: 'org.springframework.boot', name: 'spring-boot-starter', version: '[2.6.1,)' //диапазон поддерживаемых версий
//необходимо иметь в test classpath
testImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-test', version: '[2.6.1,)' //диапазон поддерживаемых версий
testImplementation group: 'ru.tinkoff.qa.neptune', name: 'spring.mock.mvc', version: LATEST_RELEASE_OR_BETA_VERSION
}
Сравнение Neptune + MockMVC с другими вариантами#
Ниже небольшое сравнение того как выглядит один и тот же тест:
с использованием Mock MVC, без реализации шагов
с использованием Mock MVC и с реализацией шагов
с использованием Mock MVC и Neptune
Тест с использованием Mock MVC, без реализации шагов#
@SpringBootTest
@AutoConfigureMockMvc
public class SomeTest {
@Autowired
private ObjectMapper objectMapper;
@Autowired
private MockMvc mockMvc;
@Test
public void semeAPITest() {
var bodyContent = mockMvc.perform(get("/something")
.contentType(APPLICATION_JSON)
.content(objectMapper.writeValueAsString(new SomeDTO())))
//Если текущее ожидание не выполнилось,
//на нем тест остановится и последующие ожидания не будут
//проверены. Хотелось бы видеть более полную картину несоответствий
//до того, как начать багофикс
.andExpect(status().isOk()) //хотелось бы частые ожидания иметь
//в более доступном и коротком виде
//
//По умолчанию не поддерживается десериализация,
//тело можно провалидировать по xpath/jsonpath/текстовому контенту
.andExpect(jsonPath("$.successOrder").value(5))
//либо получить тело ответа как сырой текст
.andReturn().getResponse().getContentAsString(StandardCharsets.UTF_8);
//и явно вызвать десериализацию
SomeResponseDto responseDto
= objectMapper.readValue(bodyContent, SomeResponseDto.class);
//дельнейшие вычисления
//проверка поля ответа
assertThat("Some field value",
responseDto.getSomething(),
is(someExpectedValue));
var someCalculatedValue = //вычисление чего-то с использованием
//responseDto
assertThat("Some calculated value",
someCalculatedValue.getSomethingElse(),
is(someExpeсtedValue2));
//и т.д.
}
}
Тест с использованием Mock MVC и с реализацией шагов#
Предположим, что результат интеграционного теста должен быть оформлен в отчет, описывающий по шагам, какие действия выполняются и каков их результат. Тогда
@SpringBootTest
@AutoConfigureMockMvc
public class SomeTest {
@Autowired
private ObjectMapper objectMapper;
@Autowired
private MockMvc mockMvc;
@Step("Подготовить тело запроса")
private String prepareRequestBody() {
var body = objectMapper.writeValueAsString(new SomeDTO());
addAttachment("Request body", "application/json", body);
return body;
}
@Step("Подготавливаем запрос GET /something")
private MockHttpServletRequestBuilder prepareRequest(String body) {
var getRequest = get("/something")
.contentType(APPLICATION_JSON)
.content(body);
//делаем аттачи, фиксируем параметры и т.д.
return getRequest;
}
@Step("Выполняем запрос и возвращаем прочитанное тело")
private SomeResponseDto getSomeResponseDto(
MockHttpServletRequestBuilder requestBuilder, int count) {
var bodyContent = mockMvc.perform(requestBuilder)
.andExpect(status().isOk())
.andExpect(jsonPath("$.successOrder").value(count))
.andReturn().getResponse().getContentAsString(StandardCharsets.UTF_8);
addAttachment("Response body", "application/json", bodyContent);
return objectMapper.readValue(bodyContent, SomeResponseDto.class);
}
@Step("Проверить тело ответа")
private void assertSomeDTO(SomeResponseDto toCheck, Object expected) {
assertThat("Some field value",
responseDto.getSomething(),
is(expected));
}
@Step("Проверить полученное у тела значение")
private void assertSomeCalculatedValue(SomeCalculatedValue toCheck, Object expected) {
assertThat("Some calculated value",
someCalculatedValue.getSomethingElse(),
is(expected));
}
@Test
public void semeAPITest() {
//Тест стал короче, НО!!!!
//У нас появился толстый слой шагов, который надо организовывать
//и поддерживать.
//Может начаться дублирование кода, если аналогичные действия
//встречаются в других тестах.
//Организация библиотеки шагов сделает тест непрозрачным,
//в перспективе затруднит модификацию/рефактринг тестов
var body = prepareRequestBody();
var request = prepareRequest(body);
var dto = getSomeResponseDto(request, 5);
assertSomeDTO(dto, someExpectedValue);
var someCalculatedValue = //вычисление чего-то с использованием
//responseDto
assertSomeCalculatedValue(someCalculatedValue, someExpectedValue2);
//и т.д.
}
}
Тест с использованием Mock MVC и Neptune#
@SpringBootTest
@AutoConfigureMockMvc
public class SomeTest {
@Autowired
private ObjectMapper objectMapper;
//Поле ниже можно не объявлять
//@Autowired
//private MockMvc mockMvc;
@Test //все описанное в тесте сформирует шаги разной вложенности
//и автоматически сформирует аттачи
public void semeAPITest() {
SomeResponseDto responseDto = mockMvcGet(
response(get("/something")
.contentType(APPLICATION_JSON)
.content(objectMapper.writeValueAsString(new SomeDTO())))
// будут проверены все ожидания
.expectStatus(200) //проваленные будут выделены в отчете
.expectJsonPathValue("$.successOrder", 5)
//предусмотрен механизм десериализации
.thenGetBody(SomeResponseDto.class)
);
check("Response body",
responseDto,
match("Some field value",
SomeResponseDto::getSomething,
is(someExpectedValue)),
match("Some calculated value",
dto -> {/*вычисление чего-то с использованием*/},
is(someExpeсtedValue2)));
}
}