WebClient цитирует свою документацию по Java — это Spring Framework
Неблокирующий реактивный клиент для выполнения HTTP-запросов, предоставляющий гибкий, реактивный API поверх базовых клиентских библиотек HTTP, таких как Reactor Netty .
В моем текущем проекте я активно использовал WebClient для обслуживания вызовов сервисов и обнаружил, что это потрясающий API, и мне нравится использование свободного интерфейса.
Рассмотрим удаленную службу, которая возвращает список «городов». Код с использованием WebClient выглядит следующим образом:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
...import org.springframework.http.MediaTypeimport org.springframework.web.reactive.function.client.WebClientimport org.springframework.web.reactive.function.client.bodyToFluximport org.springframework.web.util.UriComponentsBuilderimport reactor.core.publisher.Fluximport java.net.URIclass CitiesClient( private val webClientBuilder: WebClient.Builder, private val citiesBaseUrl: String) { fun getCities(): Flux<City> { val buildUri: URI = UriComponentsBuilder .fromUriString(citiesBaseUrl) .path("/cities") .build() .encode() .toUri() val webClient: WebClient = this.webClientBuilder.build() return webClient.get() .uri(buildUri) .accept(MediaType.APPLICATION_JSON) .exchange() .flatMapMany { clientResponse -> clientResponse.bodyToFlux<City>() } }} |
Однако сложно протестировать клиента, использующего WebClient. В этой статье я рассмотрю проблемы тестирования клиента с использованием WebClient и чистого решения.
Проблемы в издевательствах над WebClient
Эффективное модульное тестирование класса «CitiesClient» потребовало бы макета WebClient и каждого вызова метода в плавной цепочке интерфейса по следующим направлениям:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
val mockWebClientBuilder: WebClient.Builder = mock()val mockWebClient: WebClient = mock()whenever(mockWebClientBuilder.build()).thenReturn(mockWebClient)val mockRequestSpec: WebClient.RequestBodyUriSpec = mock()whenever(mockWebClient.get()).thenReturn(mockRequestSpec)val mockRequestBodySpec: WebClient.RequestBodySpec = mock()whenever(mockRequestSpec.uri(any<URI>())).thenReturn(mockRequestBodySpec)whenever(mockRequestBodySpec.accept(any())).thenReturn(mockRequestBodySpec)val citiesJson: String = this.javaClass.getResource("/sample-cities.json").readText()val clientResponse: ClientResponse = ClientResponse .create(HttpStatus.OK) .header("Content-Type","application/json") .body(citiesJson).build()whenever(mockRequestBodySpec.exchange()).thenReturn(Mono.just(clientResponse))val cities: Flux<City> = citiesClient.getCities() |
Это делает тест очень нестабильным, так как любое изменение порядка вызовов приведет к появлению новых имитаций, которые необходимо будет записать.
Тестирование с использованием реальных конечных точек
Подход, который работает хорошо, состоит в том, чтобы создать настоящий сервер, который ведет себя как цель клиента. Два фиктивных сервера, которые действительно хорошо работают — это mockwebserver в библиотеке okhttp и WireMock . Пример с Wiremock выглядит так:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
import com.github.tomakehurst.wiremock.WireMockServerimport com.github.tomakehurst.wiremock.client.WireMockimport com.github.tomakehurst.wiremock.core.WireMockConfigurationimport org.bk.samples.model.Cityimport org.junit.jupiter.api.AfterAllimport org.junit.jupiter.api.BeforeAllimport org.junit.jupiter.api.Testimport org.springframework.http.HttpStatusimport org.springframework.web.reactive.function.client.WebClientimport reactor.core.publisher.Fluximport reactor.test.StepVerifierclass WiremockWebClientTest { @Test fun testARemoteCall() { val citiesJson = this.javaClass.getResource("/sample-cities.json").readText() WIREMOCK_SERVER.stubFor(WireMock.get(WireMock.urlMatching("/cities")) .withHeader("Accept", WireMock.equalTo("application/json")) .willReturn(WireMock.aResponse() .withStatus(HttpStatus.OK.value()) .withHeader("Content-Type", "application/json") .withBody(citiesJson))) val cities: Flux<City> = citiesClient.getCities() StepVerifier .create(cities) .expectNext(City(1L, "Portland", "USA", 1_600_000L)) .expectNext(City(2L, "Seattle", "USA", 3_200_000L)) .expectNext(City(3L, "SFO", "USA", 6_400_000L)) .expectComplete() .verify() } companion object { private val WIREMOCK_SERVER = WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort()) @BeforeAll @JvmStatic fun beforeAll() { WIREMOCK_SERVER.start() } @AfterAll @JvmStatic fun afterAll() { WIREMOCK_SERVER.stop() } }} |
Здесь сервер подключается к произвольному порту, затем ему вводится поведение, а затем клиент проверяется на этом сервере и проверяется. Этот подход работает, и нет смысла смешивать его с внутренними компонентами WebClient, но технически это интеграционный тест, который будет выполняться медленнее, чем чистый модульный тест.
Модульное тестирование путем короткого замыкания удаленного вызова
Подход, который я использовал недавно, заключается в коротком замыкании удаленного вызова с использованием функции ExchangeFunction . Функция ExchangeFunction представляет фактические механизмы выполнения удаленного вызова и может быть заменена механизмом, который отвечает тем, что тест ожидает следующим образом:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
import org.junit.jupiter.api.Testimport org.springframework.http.HttpStatusimport org.springframework.web.reactive.function.client.ClientResponseimport org.springframework.web.reactive.function.client.ExchangeFunctionimport org.springframework.web.reactive.function.client.WebClientimport reactor.core.publisher.Fluximport reactor.core.publisher.Monoimport reactor.test.StepVerifierclass CitiesWebClientTest { @Test fun testCleanResponse() { val citiesJson: String = this.javaClass.getResource("/sample-cities.json").readText() val clientResponse: ClientResponse = ClientResponse .create(HttpStatus.OK) .header("Content-Type","application/json") .body(citiesJson).build() val shortCircuitingExchangeFunction = ExchangeFunction { Mono.just(clientResponse) } val webClientBuilder: WebClient.Builder = WebClient.builder().exchangeFunction(shortCircuitingExchangeFunction) val cities: Flux<City> = citiesClient.getCities() StepVerifier .create(cities) .expectNext(City(1L, "Portland", "USA", 1_600_000L)) .expectNext(City(2L, "Seattle", "USA", 3_200_000L)) .expectNext(City(3L, "SFO", "USA", 6_400_000L)) .expectComplete() .verify() }} |
WebClient внедряется с функцией ExchangeFunction, которая просто возвращает ответ с ожидаемым поведением удаленного сервера. Это привело к короткому замыканию всего удаленного вызова и позволяет всесторонне протестировать клиента. Этот подход зависит от небольших знаний внутренних компонентов WebClient. Это хороший компромисс, хотя он будет работать намного быстрее, чем тест с использованием WireMock.
Этот подход не оригинален, хотя, я основал этот тест на некоторых тестах, используемых для тестирования самого WebClient, например, здесь
Вывод
Лично я предпочитаю последний подход, он позволил мне написать достаточно всеобъемлющие модульные тесты для Клиента, использующего WebClient для удаленных вызовов. Мой проект с полностью рабочими образцами здесь .
|
Смотрите оригинальную статью здесь: Юнит тест для Spring WebClient Мнения, высказанные участниками Java Code Geeks, являются их собственными. |