Search code examples
javaspringspring-bootunit-testingspring-webflux

The mapper returned a null Mono in a SpringBoot Reactive Router and Handler Unit test


I have a functional router :

@Bean
    public RouterFunction<ServerResponse> greetingRoute(Utilities utils,
                                                     AppConfig cfg,
                                                     GreetingHandler greetingHandler) {
        return RouterFunctions.route()
                .path("/api",api -> api
                        // x-headers validation
                        .filter((request,next) -> utils.verifyXHeaders(request,cfg,next))
                        // Customer urls
                        .nest(accept(MediaType.APPLICATION_JSON),ops -> ops
                                // Greeting API
                                .GET("/v2/hello",greetingHandler::hello)
                        )
                        .build())
                .build();
    }

And it's handler function below:

public Mono<ServerResponse> hello(ServerRequest serverRequest) {

        Mono<Greeting> greetingResponse = greetingSvc.greetingFunction();

        return greetingResponse.flatMap(greeting -> {
            BaseResponse brs = BaseResponse.builder()
                    .responseCode(String.valueOf(HttpStatus.OK.value()))
                    .responseMsg("Greeting")
                    .detailedMsg("Greeting Success")
                    .responseData(greeting)
                    .build();

            return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON)
                    .bodyValue(brs);
        });

    }

I am trying to write a unit test for the handler as follows:

@Slf4j
class GreetingHandlerTest {

    GreetingSvc greetingSvc;
    WebTestClient client;

    @BeforeEach
    void setUp(){
        this.greetingSvc = mock(GreetingSvc.class);
        Utilities utils = mock(Utilities.class);
        AppConfig cfg = mock(AppConfig.class);
        Greeting greeting = new Greeting("Hello Spring");


        GreetingHandler greetingHandler = new GreetingHandler(this.greetingSvc);

        when(this.greetingSvc.greetingFunction()).thenReturn(Mono.just(greeting));


        RouterFunction<?> route = new AppRouter()
                .greetingRoute(utils,cfg,greetingHandler);

        this.client = WebTestClient.bindToRouterFunction(route)
                .build();
    }

    @Test
    @DisplayName("Get Hello Back")
    void testHello() {

        client.get()
                .uri("/api/v2/hello")
                .headers(Utilities.getHttpHeaders())
                .accept(MediaType.APPLICATION_JSON)
                .exchange()
                .expectStatus().isOk()
                .expectBody(BaseResponse.class);
        ;
    }
}

However this test always fails with an error The mapper returned a null Mono and the following stack trace:

12:46:14.282 [parallel-1] DEBUG org.springframework.web.server.adapter.HttpWebHandlerAdapter - [73a1446b] HTTP GET "/api/v2/hello"
12:46:14.334 [parallel-1] ERROR org.springframework.web.server.adapter.HttpWebHandlerAdapter - [73a1446b] 500 Server Error for HTTP GET "/api/v2/hello"
java.lang.NullPointerException: The mapper returned a null Mono
    at java.util.Objects.requireNonNull(Objects.java:228)
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
    *__checkpoint ⇢ HTTP GET "/api/v2/hello" [ExceptionHandlingWebHandler]
Original Stack Trace:
        at java.util.Objects.requireNonNull(Objects.java:228)
        at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:125)
        at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.java:74)
        at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:82)
        at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.innerNext(FluxConcatMap.java:282)
        at reactor.core.publisher.FluxConcatMap$ConcatMapInner.onNext(FluxConcatMap.java:863)
        at reactor.core.publisher.FluxPeek$PeekSubscriber.onNext(FluxPeek.java:200)
        at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:122)
        at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:82)
        at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.innerNext(FluxConcatMap.java:282)
        at reactor.core.publisher.FluxConcatMap$ConcatMapInner.onNext(FluxConcatMap.java:863)
        at reactor.core.publisher.FluxPeek$PeekSubscriber.onNext(FluxPeek.java:200)
        at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:82)
        at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.drain(FluxConcatMap.java:431)
        at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.onSubscribe(FluxConcatMap.java:219)
        at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201)
        at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83)
        at reactor.core.publisher.Mono.subscribe(Mono.java:4490)
        at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.drain(FluxConcatMap.java:451)
        at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.onSubscribe(FluxConcatMap.java:219)
        at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201)
        at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83)
        at reactor.core.publisher.Mono.subscribe(Mono.java:4490)
        at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.drain(FluxConcatMap.java:451)
        at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.onSubscribe(FluxConcatMap.java:219)
        at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201)
        at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83)
        at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64)
        at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:53)
        at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:53)
        at reactor.core.publisher.Mono.subscribe(Mono.java:4490)
        at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.subscribeNext(MonoIgnoreThen.java:263)
        at reactor.core.publisher.MonoIgnoreThen.subscribe(MonoIgnoreThen.java:51)
        at reactor.core.publisher.Mono.subscribe(Mono.java:4490)
        at reactor.core.publisher.Mono.subscribeWith(Mono.java:4605)
        at reactor.core.publisher.Mono.subscribe(Mono.java:4457)
        at reactor.core.publisher.Mono.subscribe(Mono.java:4393)
        at org.springframework.test.web.reactive.server.HttpHandlerConnector.lambda$doConnect$2(HttpHandlerConnector.java:98)
        at org.springframework.mock.http.client.reactive.MockClientHttpRequest.lambda$null$2(MockClientHttpRequest.java:124)
        at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:45)
        at reactor.core.publisher.Mono.subscribe(Mono.java:4490)
        at reactor.core.publisher.FluxConcatIterable$ConcatIterableSubscriber.onComplete(FluxConcatIterable.java:147)
        at reactor.core.publisher.FluxConcatIterable.subscribe(FluxConcatIterable.java:60)
        at reactor.core.publisher.Mono.subscribe(Mono.java:4490)
        at reactor.core.publisher.Mono.subscribeWith(Mono.java:4605)
        at reactor.core.publisher.Mono.subscribe(Mono.java:4457)
        at reactor.core.publisher.Mono.subscribe(Mono.java:4393)
        at org.springframework.test.web.reactive.server.HttpHandlerConnector.doConnect(HttpHandlerConnector.java:112)
        at org.springframework.test.web.reactive.server.HttpHandlerConnector.lambda$connect$0(HttpHandlerConnector.java:79)
        at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:45)
        at reactor.core.publisher.Mono.subscribe(Mono.java:4490)
        at reactor.core.publisher.MonoSubscribeOn$SubscribeOnSubscriber.run(MonoSubscribeOn.java:126)
        at reactor.core.scheduler.WorkerTask.call(WorkerTask.java:84)
        at reactor.core.scheduler.WorkerTask.call(WorkerTask.java:37)
        at java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:266)
        at java.util.concurrent.FutureTask.run(FutureTask.java)
        at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180)
        at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at java.lang.Thread.run(Thread.java:748)
12:46:14.346 [parallel-1] DEBUG org.springframework.test.web.reactive.server.HttpHandlerConnector - Creating client response for  GET "/api/v2/hello"
12:46:14.351 [parallel-1] DEBUG org.springframework.web.reactive.function.client.ExchangeFunctions - [68105edc] [185decc2] Response 500 INTERNAL_SERVER_ERROR

But testing the api on postman it responds with a success:

{
    "responseCode": "200",
    "responseMsg": "Greeting",
    "detailedMsg": "Greeting Success",
    "Data": {
        "message": "Hello, Spring!"
    }
}

Been stuck on this for a while now. How do I make this unit test pass by mocking the greeting service implementation. The Integration test also works by the way, am stuck on this unit test.

For more context greetingSvc implementation is here:


public Mono<Greeting> greetingFunction(){
        return Mono.just(new Greeting("Hello, Spring!"));
    }


Solution

  • The problem here was trying to mock the router params . In my case I replaced mocks for Utilities and AppConfig with real initialised objects as follows:

    Utilities utilities = new Utilities();
    AppConfig cfg = new AppConfig();
    

    I updated my test to the following code and it passed.

    @Slf4j
    class GreetingHandlerTest {
    
        GreetingSvc greetingSvc;
        WebTestClient client;
    
        @BeforeEach
        void setUp(){
            this.greetingSvc = mock(GreetingSvc.class);
    
            Utilities utilities = new Utilities();
            AppConfig cfg = new AppConfig();
    
            Greeting greeting = new Greeting("Hello Spring");
    
    
            GreetingHandler greetingHandler = new GreetingHandler(this.greetingSvc);
    
            when(this.greetingSvc.greetingFunction()).thenReturn(Mono.just(greeting));
    
    
            RouterFunction<?> route = new AppRouter().greetingRoute(cfg,utilities,greetingHandler);
    
            this.client = WebTestClient.bindToRouterFunction(route)
                    .build();
        }
    
        @Test
        @DisplayName("Get Hello Back")
        void testHello() {
    
            client.get()
                    .uri("/api/v2/hello")
                    .headers(Utilities.getHttpHeaders())
                    .accept(MediaType.APPLICATION_JSON)
                    .exchange()
                    .expectStatus().isOk()
                    .expectBody(BaseResponse.class);
            ;
        }
    }