WebTestClient#
Данный модуль:
является дополнением к стандартному Spring WebTestClient
данный модуль предоставляет фасад , который фиксирует работу Spring WebTestClient в виде шагов
данный модуль исправляет потенциальные неудобства флоу Spring WebTestClient
<dependency>
<!--необходимо иметь в classpath-->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</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.web.testclient</artifactId>
<version>${LATEST_RELEASE_OR_BETA_VERSION}</version>
<scope>test</scope>
</dependency>
dependencies {
//необходимо иметь в classpath
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-webflux', 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.web.testclient', version: LATEST_RELEASE_OR_BETA_VERSION
}
Сравнение Neptune + WebTestClient с другими вариантами#
Ниже небольшое сравнение того как выглядит один и тот же тест:
с использованием WebTestClient, без реализации шагов
с использованием WebTestClient и с реализацией шагов
с использованием WebTestClient и Neptune
Тест с использованием WebTestClient, без реализации шагов#
@SpringBootTest
@AutoConfigureWebTestClient
public class SomeTest {
@Autowired
private WebTestClient client;
@Test
public void semeAPITest() {
SomeResponseDto responseDto = client.post()
.uri("/something")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.body(Mono.just(new SomeDTO()), SomeDTO.class)
.exchange()
//Если текущее ожидание не выполнилось,
//на нем тест остановится и последующие ожидания не будут
//проверены. Хотелось бы видеть более полную картину несоответствий
//до того, как начать багофикс
.expectStatus().isOk()
//хотелось бы частые ожидания иметь
//в более доступном и коротком виде
.expectHeader().contentType(MediaType.APPLICATION_JSON)
.expectBody(SomeResponseDto.class)
.returnResult()
.getResponseBody();
//дельнейшие вычисления
//проверка поля ответа
assertThat("Some field value",
responseDto.getSomething(),
is(someExpectedValue));
var someCalculatedValue = //вычисление чего-то с использованием
//responseDto
assertThat("Some calculated value",
someCalculatedValue.getSomethingElse(),
is(someExpeсtedValue2));
//и т.д.
}
}
Тест с использованием WebTestClient и с реализацией шагов#
Предположим, что результат интеграционного теста должен быть оформлен в отчет, описывающий по шагам, какие действия выполняются и каков их результат. Тогда
@SpringBootTest
@AutoConfigureWebTestClient
public class SomeTest {
@Autowired
private ObjectMapper objectMapper;
@Autowired
private WebTestClient client;
@Step("Подготовить тело запроса")
private SomeDTO prepareRequestBody() {
var toReturn = new SomeDTO();
addAttachment("Request body", "application/json",
objectMapper.writeValueAsString(toReturn));
return toReturn;
}
@Step("Получаем ответ на запрос POST /something")
private ResponseSpec getResponsePostSpec(SomeDTO body) {
var postExchangeSpec = client.post()
.uri("/something")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.body(Mono.just(new SomeDTO()), SomeDTO.class)
.exchange();
//делаем аттачи, фиксируем параметры и т.д.
return postExchangeSpec;
}
@Step("Выполняем проверку ответа и возвращаем прочитанное тело")
private SomeResponseDto getSomeResponseDto(
ResponseSpec responseSpec) {
SomeResponseDto bodyContent = responseSpec.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
.expectBody(SomeResponseDto.class)
.returnResult()
.getResponseBody();
addAttachment("Response body", "application/json",
objectMapper.writeValueAsString(bodyContent));
return bodyContent;
}
@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 response = getResponsePostSpec(body);
var dto = getSomeResponseDto(response);
assertSomeDTO(dto, someExpectedValue);
var someCalculatedValue = //вычисление чего-то с использованием
//responseDto
assertSomeCalculatedValue(someCalculatedValue, someExpectedValue2);
//и т.д.
}
}
Тест с использованием WebTestClient и Neptune#
@SpringBootTest
@AutoConfigureWebTestClient
public class SomeTest {
//Поле ниже можно не объявлять
//@Autowired
//private WebTestClient client;
@Test //все описанное в тесте сформирует шаги разной вложенности
//и автоматически сформирует аттачи
public void semeAPITest() {
SomeResponseDto responseDto = webTestClient(
send(webClient -> webClient.post()
.uri("/something")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.body(Mono.just(new SomeDTO()), SomeDTO.class),
Dto.class)
// будут проверены все ожидания
.expectStatus(200) //проваленные будут выделены в отчете
.expectContentType(APPLICATION_JSON)
.thenGetBody()
);
check("Response body",
responseDto,
match("Some field value",
SomeResponseDto::getSomething,
is(someExpectedValue)),
match("Some calculated value",
dto -> {/*вычисление чего-то с использованием*/},
is(someExpeсtedValue2)));
}
}