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.MediaType import org.springframework.web.reactive.function.client.WebClient import org.springframework.web.reactive.function.client.bodyToFlux import org.springframework.web.util.UriComponentsBuilder import reactor.core.publisher.Flux import java.net.URI class 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.WireMockServer import com.github.tomakehurst.wiremock.client.WireMock import com.github.tomakehurst.wiremock.core.WireMockConfiguration import org.bk.samples.model.City import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test import org.springframework.http.HttpStatus import org.springframework.web.reactive.function.client.WebClient import reactor.core.publisher.Flux import reactor.test.StepVerifier class 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.Test import org.springframework.http.HttpStatus import org.springframework.web.reactive.function.client.ClientResponse import org.springframework.web.reactive.function.client.ExchangeFunction import org.springframework.web.reactive.function.client.WebClient import reactor.core.publisher.Flux import reactor.core.publisher.Mono import reactor.test.StepVerifier class 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, являются их собственными. |