Hot questions for Using Mockito in spring webflux

Top 10 Java Open Source / Mockito / spring webflux

Question:

Im currently writing some basic unit tests for my REST-Endpoints. I use Mockito for that. Here one example:

@MockBean
private MyService service;

@Test
public void getItems() {
    Flux<Item> result = Flux.create(sink -> {
        sink.next(new Item("1"));
        sink.next(new Item("2"));
        sink.complete();
    });

    Mono<ItemParams> params = Mono.just(new ItemParams("1"));

    Mockito.when(this.service.getItems(params)).thenReturn(result);

    this.webClient.post().uri("/items")
            .accept(MediaType.APPLICATION_STREAM_JSON)
            .contentType(MediaType.APPLICATION_STREAM_JSON)
            .body(BodyInserters.fromPublisher(params, ItemParams.class))
            .exchange()
            .expectStatus().isOk()
            .expectBodyList(Item.class).isEqualTo(Objects.requireNonNull(result.collectList().block()));
}

This implementation leads to the following error:

java.lang.AssertionError: Response body expected:<[Item(name=1), Item(name=2)]> but was:<[]>

> POST /items
> WebTestClient-Request-Id: [1]
> Accept: [application/stream+json]
> Content-Type: [application/stream+json]

Content not available yet

< 200 OK
< Content-Type: [application/stream+json;charset=UTF-8]

No content

When I exchange the parameter in the Mockito Statement with Mockito.any()

Mockito.when(this.service.getItems(Mockito.any())).thenReturn(result);

The test runs through successfully. That means that for some reason the params I put into the Mockito Statement isnt equal to the params object which I put into BodyInserters.fromPublisher(params, ItemParams.class)

How am I supposed to test my functionality then?

EDIT

REST-Endpoint

@PostMapping(path = "/items", consumes = MediaType.APPLICATION_STREAM_JSON_VALUE, produces = MediaType.APPLICATION_STREAM_JSON_VALUE)
public Flux<Item> getItems(@Valid @RequestBody Mono<ItemParams> itemParms) {
    return this.service.getItems(itemParms);
}

Answer:

Wouldn't the actual object, @RequestBody Mono<ItemParams> itemParms, be different than the one you create and pass in the test?

You could take advantage of thenAnswer in order to verify the content of the object that is actually passed to the service:

Mockito.when(this.service.getItems(Mockito.any()))
       .thenAnswer(new Answer<Flux<Item>>() {

    @Override
    public Flux<Item> answer(InvocationOnMock invocation) throws Throwable {
        Mono<ItemParams> mono = (Mono<ItemParams>)invocation.getArgument(0);

        if(/* verify that paseed mono contains new ItemParams("1")*/){
          return result;
        }

        return null;
    }
});

Question:

I am working on a simple project which uses Spring Boot 2 with Spring WebFlux using Kotlin. I wrote test for my handler function (in which I mock the dependencies using Mockito).

However, it seems like my route function does not trigger the handler, as all of my requests return HTTP 404 NOT FOUND (even though the route is correct).

I have looked at various other projects to find out what how these tests are supposed to be written (here, here), but the problem persists.

The code is as follows (and can also be found on GitHub):

UserRouterTest

@ExtendWith(SpringExtension::class, MockitoExtension::class)
@Import(UserHandler::class)
@WebFluxTest
class UserRouterTest {

    @MockBean
    private lateinit var userService: UserService

    @Autowired
    private lateinit var userHandler: UserHandler

    @Test
    fun givenExistingCustomer_whenGetCustomerByID_thenCustomerFound() {
        val expectedCustomer = User("test", "test")
        val id = expectedCustomer.userID

        `when`(userService.getUserByID(id)).thenReturn(Optional.ofNullable(expectedCustomer))

        val router = UserRouter().userRoutes(userHandler)
        val client = WebTestClient.bindToRouterFunction(router).build()

        client.get()
                .uri("/users/$id")
                .accept(MediaType.ALL)
                .exchange()
                .expectStatus().isOk
                .expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
                .expectBody(User::class.java)
    }
}

User

@Entity
class User(var username : String, var password: String) {

    @Id
    val userID = UUID.randomUUID()
}

UserRepository

@Repository
interface UserRepository : JpaRepository<User, UUID>{
}

UserService

@Service
class UserService(
        private val userRepository: UserRepository
) {
    fun getUserByID(id: UUID): Optional<User> {
        return Optional.of(
                try {
                    userRepository.getOne(id)
                } catch (e: EntityNotFoundException) {
                    User("test", "test")
                }
        )
    }

    fun addUser(user: User) {
        userRepository.save(user)
    }
}

UserHandler

@Component
class UserHandler(
        private val userService: UserService
) {
    fun getUserWithID(request: ServerRequest): Mono<ServerResponse> {
        val id = try {
            UUID.fromString(request.pathVariable("userID"))
        } catch (e: IllegalArgumentException) {
            return ServerResponse.badRequest().syncBody("Invalid user id")
        }
        val user = userService.getUserByID(id).get()
        return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8)
                .body(BodyInserters.fromObject(user))
    }
}

UserRouter

@Configuration
class UserRouter {
    @Bean
    fun userRoutes(userHandler: UserHandler) = router {
        contentType(MediaType.APPLICATION_JSON_UTF8).nest {
            GET("/users/{userID}", userHandler::getUserWithID)
            GET("") { ServerResponse.ok().build() }
        }
    }
}

EDIT

To route based on the presence of one or more query parameter (regardless of their values), we can do the following: UserRouter

@Configuration
class UserRouter {
    @Bean
    fun userRoutes(userHandler: UserHandler) = router {

        GET("/users/{userID}", userHandler::getUserWithID)
        (GET("/users/")
                and queryParam("username") { true }
                and queryParam("password") { true }
                )
                .invoke(userHandler::getUsers)
    }
}

Note that GET("/users/?username={username}", userHandler::getUsersWithUsername) does not work.


Answer:

The way the router is configured - contentType(MediaType.APPLICATION_JSON_UTF8).nest - will only match requests that have this content type, so you would have to either remove the contentType prerequisite or change the test to include it

client.get()
                .uri("/users/$id")
                .accept(MediaType.ALL)
                .header("Content-Type", "application/json;charset=UTF-8")
                .exchange()
                .expectStatus().isOk
                .expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
                .expectBody(User::class.java)

Question:

I would appreciate any hints on the following problem I've encountered. While unit testing multipart file upload service method in Spring Reactive WebFlux app, I am getting NPE for reactor.core.publisher.MonoWhen$WhenCoordinator as follows

java.lang.NullPointerException
    at reactor.core.publisher.MonoWhen$WhenCoordinator.subscribe(MonoWhen.java:149) 

Complete log is listed below as well.

Test :

@RunWith(SpringRunner.class)
@SpringBootTest
public class FileServiceTest2 {


@MockBean
private UploadedImageRepository uploadedImageRepository;

...

       @Test
        public void assembleImageTest() {
            UploadedImage ui1 = new UploadedImage("1", "ui1.png");
            UploadedImage ui2 = new UploadedImage("2", "ui2.png");

            FilePart filePart1 = mock(FilePart.class);
            FilePart filePart2 = mock(FilePart.class);

            given(this.uploadedImageRepository.save(ui1))
                    .willReturn(Mono.just(ui1));
            given(this.uploadedImageRepository.save(ui2))
                    .willReturn(Mono.just(ui2));
            given(this.uploadedImageRepository.findAll())
                    .willReturn(Flux.just(ui1, ui2));

            given(filePart1.filename())
                    .willReturn(ui1.getImageName());
            given(filePart1.transferTo(any()))
                    .willReturn(Mono.empty());
            given(filePart2.filename())
                    .willReturn(ui2.getImageName());
            given(filePart2.transferTo(any()))
                    .willReturn(Mono.empty());

            Flux<FilePart> files = Flux.just(filePart1, filePart2);

            StepVerifier.create(this.uploadService.createFile(files))
                    .verifyComplete();
        }

Under test :

@Service
public class UploadService {

Mono<Void> createFile(Flux<FilePart> fileParts) {
        return fileParts.flatMap(part -> {
            Mono<UploadedImage> savedToDBImage = this.uploadedImageRepository.save(
                    new UploadedImage(UUID.randomUUID().toString(), part.filename()))
                    .log("createFile-fileSavedToDB"); // NPE!

            Mono<Void> copiedFile = Mono.just(Paths.get(UPLOAD_URL, part.filename()).toFile())
                    .log("createFile-pathAssembled")
                    .doOnNext(destinationFile -> {
                        try {
                            destinationFile.createNewFile();
                        } catch (IOException e) {
                            throw new RuntimeException(e);
                        }
                    })
                    .log("createFile-fileAssembled")
                    .flatMap(part::transferTo)
                    .log("createFile-fileCopied");

            return Mono.when(savedToDBImage, copiedFile)
                    .log("createFile-monoWhen");
        })
                .log("createFile-flatMap")
                .then()
                .log("createFile-done");
    }

UploadedImage class (w Lombok) :

@Data
@RequiredArgsConstructor
@NoArgsConstructor
public class UploadedImage {

        @NonNull private String id;
        @NonNull private String imageName;

}

SpringData Reactive Repository:

@Repository
public interface UploadedImageRepository extends ReactiveCrudRepository<UploadedImage, String> {

}

Logs are as follows:

java.lang.NullPointerException
    at reactor.core.publisher.MonoWhen$WhenCoordinator.subscribe(MonoWhen.java:149)
    at reactor.core.publisher.MonoWhen.subscribe(MonoWhen.java:99)
    at reactor.core.publisher.MonoOnAssembly.subscribe(MonoOnAssembly.java:76)
    at reactor.core.publisher.MonoLogFuseable.subscribe(MonoLogFuseable.java:53)
    at reactor.core.publisher.Mono.subscribe(Mono.java:3080)
    at reactor.core.publisher.FluxFlatMap$FlatMapMain.onNext(FluxFlatMap.java:372)
    at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.onNext(FluxPeekFuseable.java:198)
    at reactor.core.publisher.FluxArray$ArraySubscription.slowPath(FluxArray.java:118)
    at reactor.core.publisher.FluxArray$ArraySubscription.request(FluxArray.java:91)
    at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.request(FluxPeekFuseable.java:138)
    at reactor.core.publisher.FluxFlatMap$FlatMapMain.onSubscribe(FluxFlatMap.java:332)
    at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.onSubscribe(FluxPeekFuseable.java:172)
    at reactor.core.publisher.FluxArray.subscribe(FluxArray.java:53)
    at reactor.core.publisher.FluxArray.subscribe(FluxArray.java:59)
    at reactor.core.publisher.FluxLogFuseable.subscribe(FluxLogFuseable.java:53)
    at reactor.core.publisher.FluxFlatMap.subscribe(FluxFlatMap.java:97)
    at reactor.core.publisher.FluxLog.subscribe(FluxLog.java:50)
    at reactor.core.publisher.MonoIgnoreElements.subscribe(MonoIgnoreElements.java:37)
    at reactor.core.publisher.MonoLog.subscribe(MonoLog.java:51)
    at reactor.core.publisher.Mono.subscribe(Mono.java:3080)
    at reactor.test.DefaultStepVerifierBuilder$DefaultStepVerifier.verify(DefaultStepVerifierBuilder.java:728)
    at reactor.test.DefaultStepVerifierBuilder$DefaultStepVerifier.verify(DefaultStepVerifierBuilder.java:700)
    at reactor.test.DefaultStepVerifierBuilder.verifyComplete(DefaultStepVerifierBuilder.java:566)
    at pb.sl.UploadService.createFile(FileServiceTest2.java:112)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:564)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:73)
    at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:83)
    at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
    at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
    at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
    at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:251)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
    at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)

Answer:

Looking more closely, I think you're not mocking your repository as expected, and in your test the repository call returns null.

You're not using a Mockito argument matcher but a concrete instance for the argument. Later in your service implementation, this method is called, but with a different instance. My guess is Mockito is using equals to check if the given matches that mock call - maybe your UploadedImage could be improved in that regard?

given(this.uploadedImageRepository.save(ui1)) should be:

given(this.uploadedImageRepository.save(any()))
.willAnswer(invocation -> Mono.just(invocation.getArgument(0)))

Question:

Let's take a look over this simple method:

public Mono<SuccessResponse> doSomething(){
        return service1.doSomething()
            .then(service2.doSomething2())
            .thenReturn(new SuccessResponse("Awesome")));
}

So basically I want to test this method for a scenario in which, service1.doSomething() will throw an error:

when(service1.doSomething()).thenReturn(Mono.error(new IllegalStateException("Something bad happened")));
when(service2.doSomething()).thenReturn(Mono.just(new SomeResponse()))

assertThatThrownBy(() -> testedService.doSomething().block())
            .isExactlyInstanceOf(IllegalStateException.class);

verify(service2, never()).doSomething(); //Why this is executed!?

My question is why service2.doSomething() is executed once? it shouldn't be executed since service1.doSomething() throw an error above...


Answer:

The reason why the service2.doSomething() method is invoked is that while a Mono can be lazy, plainly calling an operator isn't. You are eagerly calling the methods that will return lazy Monos, thus assembling a processing pipeline.

If you inline your code, it becomes a bit clearer I think:

    //exception is CREATED immediately, but USED lazily
return Mono.error(new IllegalStateException())
    //mono is CREATED immediately. The data it will emit is also CREATED immediately. But it all triggers LAZILY.
    .then(Mono.just(new SomeResponse()))
    //note that then* operators completely ignore previous step's result (unless it is an error)
    .thenReturn(new SuccessResponse("Awesome"))); 

Some operators accept Supplier or Function which provides a lazy alternative to this eager construction style. One universal way of doing that is to use Mono.defer:

public Mono<SuccessResponse> doSomething(){
        return service1.doSomething()
            .then(Mono.defer(service2::doSomething2))
            .thenReturn(new SuccessResponse("Awesome")));
}

But I'd argue that, unless service2 hides a source that is NOT lazy (eg. a Mono adapted from a CompletableFuture), the problem is not the doSomething but the test.

With the service2 mock, you are essentially testing the assembly of the chain of operators, but not if that step in the pipeline is actually executed.

One trick available in reactor-test is to wrap the Mono.just/Mono.error in a PublisherProbe. This can be used to mock a Mono like you did, but with the added feature of providing assertions on the execution of the Mono: was it subscribed to? was it requested?

//this is ultimately tested by the assertThrownBy, let's keep it that way:
when(service1.doSomething()).thenReturn(Mono.error(new IllegalStateException("Something bad happened")));

//this will be used to ensure the `service2` Mono is never actually used: 
PublisherProbe<SomeResponse> service2Probe = PublisherProbe.of(Mono.just(new SomeResponse()));
//we still need the mock to return a Mono version of our probe
when(service2.doSomething()).thenReturn(service2Probe.mono());

assertThatThrownBy(() -> testedService.doSomething().block())
            .isExactlyInstanceOf(IllegalStateException.class);

//service2 might have returned a lazy Mono, but it was never actually used:
probe.assertWasNotSubscribed();