Статьи

Модульный тест для Spring WebClient

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 citiesClient = CitiesClient(mockWebClientBuilder, "http://somebaseurl")
 
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 citiesClient = CitiesClient(WebClient.builder(), "http://localhost:${WIREMOCK_SERVER.port()}")
 
        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 citiesClient = CitiesClient(webClientBuilder, "http://somebaseurl")
 
        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, являются их собственными.