0

I am currently testing a Service performing HTTP requests with WebClient on Spring. When I'm mocking the service I get a NullPointerException on my body when I'm testing a method which performs a POST request. Here is the error :

java.lang.NullPointerException: Cannot invoke "org.springframework.web.reactive.function.client.WebClient$RequestHeadersSpec.retrieve()" because the return value of "org.springframework.web.reactive.function.client.WebClient$RequestBodySpec.body(org.reactivestreams.Publisher, java.lang.Class)" is null

Here is my code :

import com.bluelagoon.payetonkawa.dolibarr.entities.input.LoginEntity;
import com.bluelagoon.payetonkawa.dolibarr.entities.output.SuccessEntity;
import com.bluelagoon.payetonkawa.dolibarr.entities.output.UserInfoEntity;
import com.bluelagoon.payetonkawa.dolibarr.services.DolibarrInfraService;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

@Service
public class DolibarrInfraServiceImpl implements DolibarrInfraService {

    private final WebClient webClient;

    public DolibarrInfraServiceImpl(WebClient webClient) {
        this.webClient = webClient;
    }

    @Override
    public SuccessEntity login(LoginEntity login) {
        return webClient.post()
                .uri("/login")
                .body(Mono.just(login), LoginEntity.class)
                .retrieve()
                .bodyToMono(SuccessEntity.class)
                .block();
    }

   ...
}

Here is my Test class :

package com.bluelagoon.payetonkawa.dolibarr.impl;

import com.bluelagoon.payetonkawa.dolibarr.entities.input.LoginEntity;
import com.bluelagoon.payetonkawa.dolibarr.entities.output.SuccessEntity;
import com.bluelagoon.payetonkawa.dolibarr.entities.output.TokenEntity;
import com.bluelagoon.payetonkawa.dolibarr.services.DolibarrInfraService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentMatchers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

import java.net.http.HttpRequest;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.lenient;

@ExtendWith(MockitoExtension.class)
class DolibarrInfraServiceImplTest {

    @Mock
    private WebClient webClientMock;

    @Mock
    private WebClient.RequestBodyUriSpec requestBodyUriSpecMock;

    @Mock
    private WebClient.RequestBodySpec requestBodySpecMock;

    @Mock
    private WebClient.RequestHeadersUriSpec requestHeadersUriSpecMock;

    @Mock
    private WebClient.RequestHeadersSpec requestHeadersSpecMock;

    @Mock
    private WebClient.ResponseSpec responseSpecMock;

    @InjectMocks
    private DolibarrInfraServiceImpl dolibarrInfraService;

    @Test
    void loginTest_should_return_a_valid_SuccessEntity(){
        var loginEntity = new LoginEntity();
        loginEntity.setLogin("test");
        loginEntity.setPassword("test");

        var tokenEntity = new TokenEntity();
        tokenEntity.setCode(200);
        tokenEntity.setToken("test");

        var successEntity = new SuccessEntity();
        successEntity.setSuccess(tokenEntity);

        when(webClientMock.post())
                .thenReturn(requestBodyUriSpecMock);

        when(requestBodyUriSpecMock.uri(anyString()))
                .thenReturn(requestBodySpecMock);

        when(requestBodySpecMock.body(Mono.just(loginEntity), LoginEntity.class))
                .thenReturn(requestHeadersSpecMock);

        when(requestHeadersSpecMock.retrieve())
                .thenReturn(responseSpecMock);

        when(responseSpecMock.bodyToMono(ArgumentMatchers.<Class<SuccessEntity>>notNull()))
                .thenReturn(Mono.just(successEntity));


        var expectedTokenEntity = new TokenEntity();
        expectedTokenEntity.setToken("test");
        expectedTokenEntity.setCode(200);

        var expectedSuccessEntity = new SuccessEntity();
        expectedSuccessEntity.setSuccess(expectedTokenEntity);

        var resultSuccessEntity = dolibarrInfraService.login(loginEntity);

        assertEquals(expectedSuccessEntity, resultSuccessEntity);
    }


}

Am I missing something?

Thank you for your answers

Tokoro-San
  • 37
  • 1
  • 7
  • 1
    Mocking WebClient could be cumbersome and requires to know the internal implementation details. I would recommend to look at Wiremock https://stackoverflow.com/a/75115531/9068895 or MockWebServer https://stackoverflow.com/a/71428061/9068895 that allow using real HTTP calls to a local endpoint – Alex Mar 23 '23 at 14:51
  • Your mocking won't work, the one where you mock the `.body` call won't actually match the parameters as the `Mono.just` is another instance as the `Mono.just` in the method and thus doesn't match, hence Mockito will return `null`. That being said you aren't testing anything either, only if the mocking framework is working correctly. – M. Deinum Mar 23 '23 at 15:16
  • I replaced the body() method by bodyValue() and by doing the same on my test the problem disappeared. I don't really understand the difference between those 2 methods I will just read the documentation to understand – Tokoro-San Mar 23 '23 at 16:16

1 Answers1

0

For those who stumble upon this thread and finding hard time to mock any downstream calls using webClient, I hope this will help in someway.

Actual code which calls the downstream:

public Mono<String> getSomething(final MyContext myContext) {
    return myCustomWebClient
            .get()
            .uri("/my/downstream")
            .headers(headers -> {
                headers.setBearerAuth(myContext.jwtToken());
                headers.set("header1", myContext.getHeader1());
            })
            .retrieve()
            .bodyToMono(String.class));
}

Now here is my TestClass to test this piece of code from: Router > Handler > Any Other layer > WebClient

@TestPropertySource(properties = {
        "spring.config.location=classpath:application-test.yml",
        "spring.profiles.active=your-profile"
})
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class MyClassTest {

    @Autowired
    protected WebTestClient webTestClient;

    @MockBean
    private WebClient myCustomWebClient;

    @MockBean
    private WebClient.ResponseSpec responseSpec;

    @MockBean
    @SuppressWarnings("rawtypes")
    private WebClient.RequestHeadersUriSpec requestHeadersUriSpec;

    @SuppressWarnings("rawtypes")
    private WebClient.RequestHeadersSpec requestHeadersSpec;

    @Test
    @SuppressWarnings("unchecked")
    void testCustomMethod() {
        Mockito.when(requestHeadersSpec.headers(Mockito.any()))
        .thenReturn(requestHeadersSpec);

        Mockito.when(requestHeadersSpec.retrieve())
        .thenReturn(responseSpec);

       Mockito.when(myCustomWebClient.get())
       .thenReturn(requestHeadersUriSpec);

       Mockito.when(requestHeadersUriSpec.uri("/my/downstream"))
       .thenReturn(requestHeadersSpec);

       Mockito.when(responseSpec.bodyToMono(String.class))
       .thenReturn(Mono.empty());

       Mockito.when(myCustomWebClient.get().uri("/my/downstream")
       .headers(Mockito.any()).retrieve().bodyToMono(String.class))
       .thenReturn(Mono.empty());

        webTestClient.post()
            .uri("/your-end-point")
            .accept(MediaType.APPLICATION_JSON)
            .contentType(MediaType.APPLICATION_JSON)
            .header("header1", "header1Value")
            .bodyValue(YourRequestClass.builder().build())
            .exchange()
            .expectStatus()
            .is5xxServerError()
            .expectBody(YourErrorClassPojo.class)
            .value(yourErrorResponsePojo -> {
                // Assertions.assertEquals();
            })
            .consumeWith(System.out::println);
}
mfaisalhyder
  • 2,250
  • 3
  • 28
  • 38
  • When doing the tests i just changed the method .body() to bodyvalue() and i got what i expected. I think that i missed another mock for .value() – Tokoro-San Jul 18 '23 at 09:51
  • @Tokoro-San, great, that It worked. For me, I also have to mock each step of of my actual webClient calls so that it doesn't throw NPE. It is really cumbersome to mock 5,6 chained methods for WebClient flow to work. – mfaisalhyder Jul 18 '23 at 16:07