Testing WebFlux controllers

So far, we've looked at unit testing as well as slice testing for MongoDB. These are good for covering services and backend logic. The last part we need to ensure is whether the web controllers are working properly.

Spring Boot comes with automated support to help us pick the exact type of test that we want to run. Let's start with an example:

    @RunWith(SpringRunner.class) 
    @WebFluxTest(controllers = HomeController.class) 
    @Import({ThymeleafAutoConfiguration.class}) 
    public class HomeControllerTests { 
      @Autowired 
      WebTestClient webClient; 
      @MockBean 
      ImageService imageService; 
      ... 
    } 

This preceding beginning of a controller test case can be described as follows:

  • @RunWith(SpringRunner.class) ensures all of our Spring Framework and Spring Boot test annotations integrate properly with JUnit.
  • @WebFluxTest(controllers = HomeController.class) is another slice of testing which focuses on Spring WebFlux. The default configuration enables all @Controller beans and @RestController beans as well as a mock web environment, but with the rest of the autoconfiguration disabled. However, by using the controllers argument, we have confined this test case to ONLY enable HomeController.
  • @Import(...​) specifies what additional bits we want configured outside of any Spring WebFlux controllers. In this case, the Thymeleaf autoconfiguration is needed.
  • A WebTestClient bean is autowired into our test case, giving us the means to make mock web calls.
  • @MockBean signals that the ImageService collaborator bean needed by our HomeController will be replaced by a mock, which we'll configure shortly.
Even though @WebFluxTest is another slice similar to @DataMongoTest, we broke it out of the previous section, Slice Testing, because WebFlux testing comes with an extensive range of configuration options, which we will explore later in more detail.

Let's look at a test case where we get the base URL /:

    @Test 
    public void baseRouteShouldListAllImages() { 
      // given 
      Image alphaImage = new Image("1", "alpha.png"); 
      Image bravoImage = new Image("2", "bravo.png"); 
      given(imageService.findAllImages()) 
        .willReturn(Flux.just(alphaImage, bravoImage)); 
 
      // when 
      EntityExchangeResult<String> result = webClient 
        .get().uri("/") 
        .exchange() 
        .expectStatus().isOk() 
        .expectBody(String.class).returnResult(); 
 
      // then 
      verify(imageService).findAllImages(); 
      verifyNoMoreInteractions(imageService); 
      assertThat(result.getResponseBody()) 
        .contains( 
          "<title>Learning Spring Boot: Spring-a-Gram</title>") 
        .contains("<a href="/images/alpha.png/raw">") 
        .contains("<a href="/images/bravo.png/raw">"); 
    } 

We can cover the details of this last test case as follows:

  • @Test marks this method as a JUnit test case.
  • The method name, baseRouteShouldListAllImages, gives us a quick summary of what this method should verify.
  • The first three lines mock up the ImageService bean to return a Flux of two images when findAllImages gets called.
  • webClient is then used to perform a GET / using its fluent API.
  • We verify the HTTP status to be a 200 OK, and extract the body of the result into a string.
  • We use Mockito's verify to prove that our ImageService bean's findAllImages was indeed called.
  • We use Mockito's verifyNoMoreInteractions to prove that no other calls are made to our mock ImageService.
  • Finally, we use AssertJ to inspect some key parts of the HTML page that was rendered.

This test method gives us a pretty good shake out of GET /. We are able to verify that the web page was rendered with the right content. We can also verify that our ImageService bean was called as expected. And both were done without involving a real MongoDB engine and without a fully running web container.

Spring's WebFlux machinery is verified since it still includes the bits that take an incoming request for / and routes it to HomeController.index(), yielding a Thymeleaf-generated HTML page. This way, we know our controller has been wired properly. And oftentimes, this is enough to prove the web call works.

A key scenario to explore is actually fetching a file, mockingly. It's what our app does when requesting a single image. Check out the following test case:

    @Test 
    public void fetchingImageShouldWork() { 
     given(imageService.findOneImage(any())) 
      .willReturn(Mono.just( 
         new ByteArrayResource("data".getBytes()))); 
 
      webClient 
        .get().uri("/images/alpha.png/raw") 
        .exchange() 
        .expectStatus().isOk() 
        .expectBody(String.class).isEqualTo("data"); 
      verify(imageService).findOneImage("alpha.png"); 
      verifyNoMoreInteractions(imageService); 
    } 

This preceding test case can be described as follows:

  • @Test flags this method as a JUnit test case.
  • The method name, fetchingImageShouldWork, hints that this tests successful file fetching.
  • The ImageService.findOneImage method returns a Mono<Resource>, so we need to assemble a mock resource. That can be achieved using Spring's ByteArrayResource, which takes a byte[]. Since all Java strings can be turned into byte arrays, it's a piece of cake to plug it in.
  • webClient calls GET /images/alpha.png/raw.
  • After the exchange() method, we verify the HTTP status is OK.
  • We can even check the data content in the body of the HTTP response given that the bytes can be curried back into a Java string.
  • Lastly, we use Mockito's verify to make sure our mock was called once and in no other way.

Since we're coding against a very simple interface, Resource, we don't have to go through any complicated ceremony of staging a fake test file and having it served up. While that's possible, Mockito makes it easy to stand up stubs and mocks. Additionally, Spring's assortment of Resource implementations lets us pick the right one. This reinforces the benefit of coding services against interfaces and not implementations when possible.

The other side of the coin when testing file retrieval is to verify that we properly handle file errors. What if we attempted to fetch an image but for some reason the file on the server was corrupted? Check it out in the following test code:

    @Test  
    public void fetchingNullImageShouldFail() throws IOException { 
      Resource resource = mock(Resource.class); 
      given(resource.getInputStream()) 
        .willThrow(new IOException("Bad file")); 
      given(imageService.findOneImage(any())) 
        .willReturn(Mono.just(resource)); 
 
      webClient 
        .get().uri("/images/alpha.png/raw") 
        .exchange() 
        .expectStatus().isBadRequest() 
        .expectBody(String.class) 
        .isEqualTo("Couldn't find alpha.png => Bad file"); 
 
      verify(imageService).findOneImage("alpha.png"); 
      verifyNoMoreInteractions(imageService); 
    } 

This preceding test of a failure can be described as follows:

  • @Test flags this method as a JUnit test case.
  • The method name, fetchingNullImageShouldFail, hints that this test is aimed at a failure scenario.
  • We need to mock out the file on the server, which is represented as a Spring Resource. That way, we can force it to throw an IOException when getInputStream is invoked.
  • That mock is returned when ImageService.findOneImage is called. Notice how we use Mockito's any() to simplify inputs?
  • webClient is again used to make the call.
  • After the exchange() method is made, we verify that the HTTP status is a 400 Bad Request.
  • We also check the response body and ensure it matches the expected body from our controller's exception handler.
  • Finally, we use Mockito to verify that our mock ImageService.findOneImage() was called once (and only once!) and that no other calls were made to this mock bean.

This test case shows a critical skill we all need to polish: verifying that the path of failure is handled properly. When a manager asks what if the file isn't there?, we can show them a test case indicating that we have covered it. Say we write a try...catch clause in the our code, like this one in HomeController.oneRawImage():

    return imageService.findOneImage(filename) 
    .map(resource -> { 
      try { 
        return ResponseEntity.ok() 
        .contentLength(resource.contentLength()) 
        .body(new InputStreamResource( 
          resource.getInputStream())); 
      } catch (IOException e) { 
        return ResponseEntity.badRequest() 
        .body("Couldn't find " + filename + 
          " => " + e.getMessage()); 
      } 
    }); 

We should immediately start thinking of two test cases: one test case for the try part when we can find the file and return an OK, and another test case for the catch part when IOException gets thrown and we return a Bad Request.

While it's not hard to think up all the successful scenarios, capturing the failure scenarios and testing them is important. And Mockito makes it quite easy to mock failing behavior. In fact, it's a common pattern to have one mock return another, as we did in this test case.

Mockito makes it easy to mock things left and right. Just keep sight of what you're really trying to test. One can get so caught up in mocking so that all that gets tested are the mocks. We must be sure to verify the actual behavior of the code, or the test will be meaningless.

Another webish behavior that happens all the time is processing a call and then redirecting the client to another web location. This is exactly the behavior when we issue an HTTP DELETE to our site. The URL is expected to carry the resource that must be deleted. Once completed, we need to instruct the browser to go back to the home page.

Check out the following test case:

    @Test 
    public void deleteImageShouldWork() { 
      Image alphaImage = new Image("1", "alpha.png"); 
      given(imageService.deleteImage(any())).willReturn(Mono.empty()); 
 
      webClient 
        .delete().uri("/images/alpha.png") 
        .exchange() 
        .expectStatus().isSeeOther() 
        .expectHeader().valueEquals(HttpHeaders.LOCATION, "/"); 
 
      verify(imageService).deleteImage("alpha.png"); 
      verifyNoMoreInteractions(imageService); 
    } 

We can describe this preceding redirecting web call as follows:

  • The @Test flags this method as a JUnit test case.
  • We prep our ImageService mock bean to handle a deleteImage by returning Mono.empty(). This is the way to construct a Mono<Void> object, which represents the promise that our service hands us when deletion of the file and its corresponding MongoDB record are both completed.
  • webClient performs a DELETE /images/alpha.png.
  • After the exchange() is complete, we verify the HTTP status is 303 See Other, the outcome of a Spring WebFlux redirect:/ directive.
  • As part of the HTTP redirect, there should also be a Location header containing the new URL, /.
  • Finally, we confirm that our ImageService mock bean's deleteImage method was called and nothing else.

This proves that we have properly invoked our service and then followed it up with a redirect back to the home page. It's actually possible to grab that Location header and issue another webClient call, but there is no point in this test case. We have already verified that behavior.

However, imagine that the redirect included some contextual thing like redirect:/?msg=Deleted showing a desire to bounce back to the home page but with extra data to be shown. That would be a great time to issue a second call and prove that this special message was rendered properly.

Now we can run the entire test case and see green bubbles all the way down:

We have used Mockito quite a bit but we aren't going to delve into all its features. For that, I recommend reading Mockito Cookbook written by Spring teammate Marcin Grzejszczak (@MGrzejszczak).
..................Content has been hidden....................

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