1
votes

I have a simple repository implementation like this.

@Repository
public interface PolicyRepository extends ReactiveMongoRepository<Policy, String> {

    @Query("{ id: { $exists: true }}")
    Flux<Policy> findAllPaged(Pageable pageable);
    @Query("{ name: { $eq: ?0 }}")
    Mono<Policy> findByName(String name);
}

And A simple Action Method on a controller like this.

    @ResponseStatus(HttpStatus.CREATED)
    public Mono<ResponseEntity<String>> createPolicy(@Valid @RequestBody Policy policy) {
        //Use The Mongodb ID Generator
        policy.setId(ObjectId.get().toString());
        return policyRepository.findByName(policy.getName()).flatMap(policy1 -> {
            return Mono.just(ResponseEntity.badRequest().body("A Policy with the same name as the policy you are trying to create" +
                    "already exists"));
  }).switchIfEmpty(
          policyRepository.save(policy).map(p2 ->{
                    eventPublisher.publish(Events.POLICY_CREATED, p2.getId());
            return ResponseEntity.status(HttpStatus.CREATED).body("Policy definition created successfully");
                }));
    }

What I was looking to achieve was return a bad request if there exists a policy with the same name as the one being inserted or do the save operation if the findByName method returns empty.

The strange behavior is that The following test fails because save is always called whether or not findByName returns data or not.

Here is the test

@Test
    void testCreateDuplicatePolicyShouldFail() {
        given(policyRepository.findByName(eq(policy.getName()))).willReturn(Mono.just(policy));
        given(policyRepository.save(any(Policy.class))).willReturn(Mono.just(policy));
        given(eventPublisher.publish(Events.POLICY_CREATED, policy.getId())).willReturn(Mono.just(0L));
        webTestClient.post().uri("/policies")
                .syncBody(policy)
                .exchange()
                .expectStatus().isBadRequest();
        verify(policyRepository, times(1)).findByName(eq(policy.getName()));
        verify(policyRepository, times(0)).save(any(Policy.class));
        verify(eventPublisher, times(0)).publish(Events.POLICY_CREATED, policy.getId());
    }

And it fails with the following exception

org.mockito.exceptions.verification.NeverWantedButInvoked: 
com.management.dashboard.repository.PolicyRepository#0 bean.save(
    <any com.management.core.model.Policy>
);

Please am I doing something wrong. Any pointer will be deeply appreciated.

3

3 Answers

4
votes

The problem with that mock setup is that save() IS always invoked. The Mono returned by the real repository is lazy, so nothing happens until it is subscribed to. And the job of switchIfEmpty is to make said subscription only if it received no onNext signal.

The method call is just that, a method call. Nothing switchIfEmpty can do to prevent save from being executed in this form. It's as if you had something like System.out.println(getMessage()): getMessage is called whenever that whole line is executed ;)

In order to test, what you can do is use reactor-test's PublisherProbe in the mock:

@Test
void testCreateDuplicatePolicyShouldFail() {

    //set up a probe to verify that the save Mono is never triggered
    PublisherProbe probe = PublisherProbe.of(Mono.just(policy));
    //now let the `save` return the probe:
    given(policyRepository.save(any(Policy.class))).willReturn(probe.mono());

    //rest of the mock and invocation is same
    given(policyRepository.findByName(eq(policy.getName()))).willReturn(Mono.just(policy));
    given(eventPublisher.publish(Events.POLICY_CREATED, policy.getId())).willReturn(Mono.just(0L));
    webTestClient.post().uri("/policies")
            .syncBody(policy)
            .exchange()
            .expectStatus().isBadRequest();
    verify(policyRepository, times(1)).findByName(eq(policy.getName()));
    verify(eventPublisher, times(0)).publish(Events.POLICY_CREATED, policy.getId());

    //but we now actually expect the save() to be invoked, but the probe to be inert:
    verify(policyRepository, times(1)).save(any(Policy.class));
    probe.assertWasNotSubscribed();

}
1
votes

Can you please confirm that in the test you are setting up an empty mono.

Can you please replace the below line:

given(policyRepository.findByName(eq(policy.getName()))).willReturn(Mono.just(policy));

With this line:

given(policyRepository.findByName(eq(policy.getName()))).willReturn(Mono.empty());

The switchIfEmpty operator is only called if the Stream is Empty. Moreover, you can enable log to trace the flow as well. This can be done by adding a log operator after switchIfEmpty. e.g.

 return policyRepository.findByName()
                        .switchIfEmpty()
                        .log();
1
votes

I faced the same error and the solution for it was using the Mono.defer(()->...) inside the .switchIfEmpty() function, due switch if is expected to execute always without side effects according to this medium article.