Spring WebClient的单元测试

本文探讨了在使用Spring WebClient进行服务到服务调用时遇到的单元测试挑战,包括模拟WebClient的复杂性以及如何通过短路远程调用来实现更稳定的单元测试。作者分享了使用ExchangeFunction来代替实际远程调用,从而加速测试执行的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

WebClient引用其Java文档是Spring Framework的

非阻塞,反应式客户端执行HTTP请求,通过底层HTTP客户端库(如Reactor Netty)公开流利的,反应式API

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

考虑一个返回“城市”列表的远程服务。 使用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中的两个模拟服务器运行得很好,它们是模拟服务器。 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

### Spring Boot 项目重构中的测试工具推荐 对于Spring Boot项目的重构工作,选择合适的测试工具有助于提高开发效率并确保代码质量。基于此需求,`spring-boot-starter-test`是一个不可或缺的选择[^3]。 #### spring-boot-starter-test 这个依赖项集成了多个流行的Java测试框架和技术栈,具体来说: - **JUnit**: 提供了一套完整的单元测试解决方案,允许开发者编写简洁有效的测试案例。 - **Mockito**: 是一个优秀的模拟对象框架,可以轻松创建复杂的测试场景而无需实际调用外部资源或服务。 - **AssertJ**: 提供了更加流畅自然的语言表达方式来进行断言操作,使得测试结果更易于理解和维护。 通过引入上述组件的支持,在进行诸如将原有Spring MVC控制器迁移到Spring Boot的RestController风格以及采用Component注解来实现服务层Bean自动装配等工作时能够极大地方便验证逻辑正确性和功能完整性[^4]。 此外,为了更好地适应不同层次(如控制层、业务逻辑层)的具体特点,还可以考虑结合其他专门化的测试手段,比如集成测试中可能需要用到TestRestTemplate或者WebClient去发起HTTP请求;而对于数据库交互部分,则有@DataJpaTest这样的专用注解可供选用[^1]。 ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值