Unit Tests for Spring's WebClient
Learn how to program a unit test using Spring's Web Client.
Join the DZone community and get the full member experience.
Join For FreeWebClient, to quote its Java documentation, is the Spring Framework's:
"Non-blocking, reactive client to perform HTTP requests, exposing a fluent, reactive API over underlying HTTP client libraries such as Reactor Netty."
In my current project, I have been using WebClient
extensively in making service-to-service calls and have found it to be an awesome API — I love its use of fluent interface.
Consider a remote service that returns a list of "Cities
." A code using WebClient
looks like this:
...
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>()
}
}
}
It is difficult to test a client making use of WebClient
though. In this post, I will go over the challenges in testing a client using WebClient
and a clean solution.
Challenges in Mocking WebClient
An effective unit test of the "CitiesClient
" class would require mocking of WebClient
and every method call in the fluent interface chain along these lines:
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()
This makes for an extremely flaky test as any change in the order of calls would result in new mocks that will need to be recorded.
Testing Using Real Endpoints
An approach that works well is to bring up a real server that behaves like the target of a client. Two mock servers that work really well are mockwebserver in okhttp library and WireMock. An example of Wiremock looks like this:
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()
}
}
}
Here, a server is being brought up at a random port; it is then injected with behavior, and then the client is tested against this server and validated. This approach works and there is no muddling with the internals of WebClient
in mocking this behavior, but technically, this is an integration test and will be slower to execute than a pure unit test.
Unit Testing by Short-Circuiting the Remote Call
An approach that I have been using recently is to short circuit the remote call using an ExchangeFunction. An ExchangeFunction
represents the actual mechanisms in making the remote call and can be replaced with one that responds with what the test expects the following way:
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()
}
}
The WebClient
is injected with an ExchangeFunction
, which simply returns a response with the expected behavior of the remote server. This has short-circuited the entire remote call and allows the client to be tested comprehensively. This approach depends on a little knowledge of the internals of the WebClient
. This is a decent compromise, though, as it would run far faster than a test using WireMock.
This approach is not original though; I have based this test on some of the tests used for testing WebClient
itself, for e.g., this one here.
Conclusion
I personally prefer the last approach; it has enabled me to write fairly comprehensive unit tests for a Client
making use of WebClient
for remote calls. My project with fully working samples is here.
Further Reading
Published at DZone with permission of Biju Kunjummen, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments