简体   繁体   中英

Spring Webflux Mockito - mock the response of a Webclient call

Small question regarding how to "force" or "mock" the response of a Webclient http call during unit test phase please.

I have a very straightforward method which does:

public String question() {
  String result = getWebClient().mutate().baseUrl(someUrlVariable).build().post().uri("/test").retrieve().bodyToMono(String).block();
   if (result == null) {
     doSomething1();
   }
 if (result.equals("")) {
     doSomething2();
   }
 if (result.equals("foo")) {
     doSomething3();
   }

As you can see, the complex part of this method is the Webclient call. It has (in this example) 7.method() like.mutate(), .post(). etc...

In my use case, I am not that interested testing this Webclient, at all.

What I would like to have with Mockito, is somehow the equivalent of:

public String question() {
  // it is just unit test. Mockito, please just return me the string I tell you to return please. Don't even execute this next line if possible, just return me this dummy response
  String result = the-thing-I-tell-mockito-to-return;
   if (result == null) {
     doSomething1();
   }
 if (result.equals("")) {
     doSomething2();
   }
 if (result.equals("foo")) {
     doSomething3();
   }

So far, I tried Mockito doNothing(), or Mockito.when(getWebclient()... ) of the entire line plus.thenReturn, but no luck.

How to achieve such please?

Thank you

You have to first ensure that getWebclient() returns a mock. Based on your existing code example I can't tell if that's coming for a different class or is a private method (it might make sense to inject the WebClient or WebClient.Builder via the constructor).

Next, you have to mock the whole method chain with Mockito. This includes almost copy/pasting your entire implementation:

when(webClient.mutate()).thenReturn(webClient);
when(webClient.baseUrl(yourUrl)).thenReturn(...);
// etc. 

Mockito can return deep stubs (check the documentation and search for RETURN_DEEP_STUBS ) that could simplify this stubbing setup.

However, A better solution would be to spawn a local HTTP server for your WebClient test and mock the HTTP responses. This involves less Mockito ceremony and also allows testing error scenarios (different HTTP responses, slow responses, etc.),

I would like to avoid those copy/pasting of when()

Well you have designed your code so that the only way to test it is by copy pasting of when .

So how have you designed it? well you have mixed API-code with logic which is something you should not do. The first thing you need to think about when writing tests is " What is it i want to test? " and the answer is usually Business logic .

If we look at your code:

public String question() {
    // This is api code, we dont want to test this, 
    // spring has already tested this for us.
    String result = getWebClient()
                     .mutate()
                     .baseUrl(someUrlVariable)
                     .build()
                     .post()
                     .uri("/test")
                     .retrieve()
                     .bodyToMono(String)
                     .block();

    // This is logic, this is want we want to test
    if (result == null) {
        doSomething1();
    }
    if (result.equals("")) {
         doSomething2();
    }
    if (result.equals("foo")) {
        doSomething3();
    }
}

When we design an application, we divide it into layers, usually a front facing api (RestController), then the business logic in the middle (Controllers) and lastly different resources that call other apis (repositories, resources etc.)

So when it comes to your application i would redesign it, split up the api and the logic:

@Bean
@Qualifier("questionsClient") 
public WebClient webclient(WebClient.Builder webClient) {
    return webClient.baseUrl("https://foobar.com")
                .build();
}

// This class responsibility is to fetch, and do basic validation. Ensure
// That whatever is returned from its functions is a concrete value.
// Here you should handle things like basic validation and null.
@Controller
public class QuestionResource {

    private final WebClient webClient;
    
    public QuestionResource(@Qualifier("questionsClient") WebClient webClient) {
        this.webClient = webClient;
    }


    public String get(String path) {
        return webClient.post()
                     .uri(path)
                     .retrieve()
                     .bodyToMono(String)
                     .block();
    }
}


// In this class we make business decisions on the values we have.
// If we get a "Foo" we do this. If we get a "Bar" we do this.
@Controller
public class QuestionHandler {

    private final QuestionResource questionResource;
    
    public QuestionResource(QuestionResource questionResource) {
        this.questionResource = questionResource;
    }

    public String get() {
        final String result = questionResource.get("/test");
        
        // also i dont see how the response can be null.
        // Null should never be considered a value and should not be let into the logic. 
        // Because imho. its a bomb. Anything that touches null will explode (NullPointerException). 
        // Null should be handled in the layer before.
        if (result == null) {
            return doSomething1();
        }
        if (result.equals("")) {
             return doSomething2();
        }
        if (result.equals("foo")) {
             return doSomething3();
        }
    }
}

Then in your test:

@Test
public void shouldDoSomething() {
    final QuestionResource questionResourceMock = mock(QuestionResource.class);
    when(questionResourceMock.get("/test")).thenReturn("");
    
    final QuestionHandler questionHandler = new QuestionHandler(questionResourceMock);
    final String something = questionHandler.get();
    
    // ...
    // assert etc. etc.
}

Also, i suggest you don't mutate webclients, create one webclient for each api because it gets messy fast.

This is written without an IDE, so there might be compile errors etc. etc.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM