webclient

webclient

WebClient引用其Java文档是Spring Framework的非阻塞的React式客户端执行HTTP请求,通过底层HTTP客户端库(如Reactor Netty)公开流利的React式API 。

在我当前的项目中,我广泛使用WebClient进行服务到服务的调用,并发现它是一个了不起的API,并且我喜欢使用流畅的界面。

考虑一个返回“城市”列表的远程服务。 使用WebClient的代码如下所示:

 ...
 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
 CitiesClient( 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以及流利的接口链中的每个方法调用,包括:

 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()

这使得测试非常不稳定,因为调用顺序的任何更改都将导致需要记录新的模拟。

使用真实端点进行测试

一种行之有效的方法是启动一个行为像客户端目标的真实服务器。 okhttp库和WireMock中的两个模拟服务器确实运行良好,它们是mockwebserver 。 Wiremock的示例如下所示:

 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
 WiremockWebClientTest { 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表示进行远程调用的实际机制,可以用一种以测试期望的方式响应的方式代替ExchangeFunction:

 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

CitiesWebClientTest { 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进行远程调用的客户端编写相当全面的单元测试。 我的项目具有完整的工作样本,在这里。

翻译自: https://www.javacodegeeks.com/2019/09/unit-test-springs-webclient.html

webclient

更多推荐