Hot questions for Using Mockito in 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 Mono
s, 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();