Testing Controllers with WebTestClient 

Imagine that we test a Payment service. In this scenario, suppose that the Payment service supports the GET and POST methods for the /payments endpoint. The first HTTP call is responsible for retrieving the list of executed payments for the current user. In turn, the second makes it possible to submit a new payment. Implementation of that rest controller looks like the following:

@RestController
@RequestMapping("/payments")
public class PaymentController {
   private final PaymentService paymentService;

public PaymentController(PaymentService paymentService) {
this.paymentService = paymentService;
} @GetMapping("/") public Flux<Payment> list() { return paymentService.list(); } @PostMapping("/") public Mono<String> send(Mono<Payment> payment) { return paymentService.send(payment); } }

The first step toward the verification of our service is writing all expectations from the web exchange with the service. For interaction with WebFlux endpoints, the updated spring-test module offers us the new org.springframework.test.web.reactive.server.WebTestClient class. WebTestClient is similar to org.springframework.test.web.servlet.MockMvc. The only difference between those test web clients is that WebTestClient is aimed at testing WebFlux endpoints. For example, using WebTestClient and the Mockito library, we may write the verification for retrieving the list of user payments in the following way:

@Test
public void verifyRespondWithExpectedPayments() {
   PaymentService paymentService = Mockito.mock(PaymentService.class);
   PaymentController controller = new PaymentController(paymentService);

   prepareMockResponse(paymentService);
    
   WebTestClient
      .bindToController(controller)
      .build()
      .get()
      .uri("/payments/")
      .exchange()
      .expectHeader().contentTypeCompatibleWith(APPLICATION_JSON)
      .expectStatus().is2xxSuccessful()
      .returnResult(Payment.class)
      .getResponseBody()
      .as(StepVerifier::create)
      .expectNextCount(5)
      .expectComplete()
      .verify();
  }

In this example, we built a verification of PaymentController using WebTestClient. In turn, using the WebTestClient fluent API, we may check the correctness of the response's status code and headers. Also, we can use .getResponseBody() to get a Flux of responses, which is finally verified using StepVerifier. This example shows how easily both tools can be integrated with each other.

In the preceding example, we might see that PaymentService is mocked and we do not communicate with external services when testing PaymentController. However, to check the system integrity, we have to run complete components, not just a few layers. To run the completed integration test, we need the whole application to be started. For that purpose, we may use a common @SpringBootTest annotation in combination with @AutoConfigureWebTestClientWebTestClient provides the ability to establish an HTTP connection with any HTTP server. Furthermore, WebTestClient can directly bind to WebFlux-based applications using mock request and response objects, eliminating the need for an HTTP server. It plays the same role in testing WebFlux applications as TestRestTemplate for WebMVC applications, as shown in the following code:

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureWebTestClient
public class PaymentControllerTests {
    @Autowired
    WebTestClient client;
    ...
}

Here, we do not need to configure WebTestClient anymore. This is because all of the required defaults are configured by Spring Boot's autoconfiguration.

Please note that if an application uses the Spring Security module, additional configuration for tests might be required. We may add a dependency on the spring-security-test module that provides the @WithMockUser annotation targeted especially at mocking user authentication. Out of the box, the @WithMockUser mechanism supports WebTestClient. However, even if @WithMockUser does its job, enabled by default CSRF may add some undesirable obstacles to the end-to-end tests. Pay attention; additional configuration regarding CSRF is required only for @SpringBootTest or any other Spring Boot test runner except @WebFluxTest, which disables CSRF by default.

To test the second part of the Payment service example, we need to look through the business logic of the PaymentService implementation. This is defined using the following code:

@Service
public class DefaultPaymentService implements PaymentService {

   private final PaymentRepository paymentRepository;
   private final WebClient         client;

   public DefaultPaymentService(PaymentRepository repository, 
WebClient.Builder builder) { this.paymentRepository = repository; this.client = builder.baseUrl("http://api.bank.com/submit").build(); } @Override public Mono<String> send(Mono<Payment> payment) { return payment
.zipWith(
ReactiveSecurityContextHolder.getContext(),
(p, c) -> p.withUser(c.getAuthentication().getName())
)
.flatMap(p -> client
.post() .syncBody(p) .retrieve() .bodyToMono(String.class) .then(paymentRepository.save(p))) .map(Payment::getId);
} @Override public Flux<Payment> list() { return ReactiveSecurityContextHolder .getContext() .map(SecurityContext::getAuthentication) .map(Principal::getName) .flatMapMany(paymentRepository::findAllByUser); } }

First of all, the thing that should be noted here is that the method that returns all the user's payments interacts only with the database. In contrast, the payment's submission logic, along with the database interaction, requires additional interaction with an external system via WebClient. In our example, we are using a reactive Spring Data MongoDB module, which supports an embedded mode for testing purposes. In contrast, interaction with an external bank provider, for instance, cannot be embedded. Consequently, we need to mock an external service with a tool such as WireMock (http://wiremock.org) or somehow mock outgoing HTTP requests. Mocking services with WireMock is a valid option for both WebMVC and WebFlux.

However, when comparing WebMVC and WebFlux functionality from a test perspective, the former has an advantage, because of the out-of-the-box ability to mock an outgoing HTTP interaction. Unfortunately, there is no support for a similar functionality with WebClient in Spring Boot 2.0 and Spring Framework 5.0.x. However, there is a trick here that makes it possible to mock the response of an outgoing HTTP call. In situations in which developers follow the technique of constructing a WebClient using WebClient.Builder, it is possible to mock org.springframework.web.reactive.function.client.ExchangeFunction, which plays an essential role in WebClient request processing, as shown in the following code:

public interface ExchangeFunction {
   Mono<ClientResponse> exchange(ClientRequest request);
   ...
}

Using the following test's configuration, it is possible to customize WebClient.Builder and provide a mocked, or stubbed implementation of ExchangeFunction:

@TestConfiguration
public class TestWebClientBuilderConfiguration {
   @Bean
   public WebClientCustomizer testWebClientCustomizer(
ExchangeFunction exchangeFunction
) { return builder -> builder.exchangeFunction(exchangeFunction); } }

This hack gives us the ability to verify the correctness of the formed ClientRequest. In turn, by properly implementing ClientResponsewe may simulate the network activity and interaction with the external service. The completed test may look like the following:

@ImportAutoConfiguration({
   TestSecurityConfiguration.class,
   TestWebClientBuilderConfiguration.class
})
@RunWith(SpringRunner.class)
@WebFluxTest
@AutoConfigureWebTestClient
public class PaymentControllerTests {
   @Autowired
   WebTestClient client;
    
@MockBean ExchangeFunction exchangeFunction; @Test @WithMockUser public void verifyPaymentsWasSentAndStored() { Mockito
.when(exchangeFunction.exchange(Mockito.any())) .thenReturn(
Mono.just(MockClientResponse.create(201, Mono.empty()))); client.post() .uri("/payments/") .syncBody(new Payment()) .exchange() .expectStatus().is2xxSuccessful() .returnResult(String.class) .getResponseBody() .as(StepVerifier::create) .expectNextCount(1) .expectComplete() .verify(); Mockito.verify(exchangeFunction).exchange(Mockito.any()); } }

In this example, we used the @WebFluxTest annotation to disable full autoconfiguration and apply only WebFlux's relevant configuration, including WebTestClient. The @MockBeen annotation is used to inject a mocked instance of ExchangeFunction into the Spring IoC container. In turn, the combination of Mockito and WebTestClient allows us to build an end-to-end verification of the desired business logic.

Even though it is possible to mock outgoing HTTP communication in WebFlux applications in a similar way to WebMVC, do this with caution! This approach has its pitfalls. Now, application tests are built under the assumption that all HTTP communications are implemented with WebClient. This is not a service contract, but rather an implementation detail. Consequently, if any service changes its HTTP client library for any reason, the corresponding tests will break for no reason. Consequently, it is preferable to use WireMock to mock external services. Such an approach not only assumes an actual HTTP client library, but also tests actual request-response payloads sent over the network. As a rule of thumb, it may be acceptable to mock the HTTP client library when testing separate classes with business logic, but it is a definite no-go for black-box testing the entire service.

In general, in the following techniques, we may verify all business logic built on top of a standard Spring WebFlux API. WebTestClient has an expressive API that allows us to verify regular REST controllers and new Router functions using .bindToRouterFunction() or  .bindToWebHandler(). Moreover, using WebTestClientwe may perform black-box testing by using .bindToServer() and providing a full server HTTP address. The following test checks some assumptions regarding the website http://www.bbc.com and fails (as expected) because of the difference between the expected and actual response bodies:

WebTestClient webTestClient = WebTestClient
.bindToServer()
.baseUrl("http://www.bbc.com")
.build();

webTestClient
.get()
.exchange()
.expectStatus().is2xxSuccessful()
.expectHeader().exists("ETag")
.expectBody().json("{}");

This example shows that WebFlux's WebClient and WebTestClient classes not only give us asynchronous non-blocking clients for HTTP communication, but also a fluent API for integration testing.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.119.106.237