0

Having problem with params capturing in junit test.

I have the method which send person object to another service. Also I use circuitBreaker to continue app run, even if person service is not available.

Circuit-Breaker bean configuration:

@Bean
public CircuitBreaker circuitBreaker(CircuitBreakerFactory<Resilience4JCircuitBreakerConfiguration, Resilience4JConfigBuilder> factory) {
    return factory.create("circuitBreaker");
}
@Autowired
private PersonClient personClient;

@Autowired
private CircuitBreaker circuitBreaker;

public void save(Person person) {
        //setting some fields into person.
        circuitBreaker.run(() -> personClient.sendPerson(person), throwable -> {
            log.error("Error sending message to PersonService: ", throwable);
            return null;
        });
    }
}

I am having trouble testing this method. Here is my test:

@InjectMocks
private CustomerServiceImpl customerService;

@Mock
private PersonClient personClient;

@Captor
private ArgumentCaptor<Person> captor;

@Mock
private CircuitBreaker circuitBreaker;

@Test
    void savePerson() {
        //person obj created here;

        when(personClient.sendPerson(any(Person.class))).thenReturn(Response.class);

        customerService.save(obj);


        verify(personClient).sendPerson(captor.capture()); // failing here 
        verifyNoMoreInteractions(personClient);

        var testPerson = captor.getValue();
//assertions

Here is fail description: Wanted but not invoked:personClient.sendPerson();

Is there a way to capture argument here? I captured it without having circuitBreaker, but with circuitBreaker (call the service 3 times, otherwise return nothing) having fail test.

2 Answers2

0

It seems sendPerson may not be getting invoked in your test due to the circuit breaker.

Create a new circuit breaker instance specifically for the test, then configure it to always allow the call to go through.

void setUp() {
    circuitBreaker = circuitBreakerRegistry.circuitBreaker("test-circuit-breaker");
    circuitBreaker.transitionToClosedState();
}

@Test
void savePerson() {
    // Create the person object
    Person person = new Person();
    person.setName("Alice"); 
    person.setAge(20); 

    // Configure the mock response
    when(personClient.sendPerson(any(Person.class))).thenReturn(ResponseEntity.ok().build());

    // Call the save method
    customerService.save(person);

    // Verify that the sendPerson method was called with the correct argument
    verify(personClient).sendPerson(argThat(argument -> argument.getName().equals("Alice") && argument.getAge().equals(20));
}
muhrahim95
  • 121
  • 4
0

Your circuit breaker is defined as @Mock. A mock does nothing, unless you tell it to. So all your save method does is call a "do nothing method" (.run).

My recommendation would be to use a real circuit breaker or an implementation which can be used in a test (e.g. an implementation which simply executes the runnable/callable parameter immediately).

If no such implementation is available, you can set up the stubbing of the mock manually (but this gets messy really quickly).

Assuming you are using Spring cloud's CircuitBreaker class, put this in your setup method or at the beginning of your test method(s):

when(circuitBreaker.run(any()))
      .thenAnswer(a -> a.<Supplier<Object>getArgument(0).get());

This will instruct the mock to call the lambda in its first argument immediately and return its result, as soon as run is called.

You are not using your captor anywhere in your calls, so it cannot possibly capture any values. Fortunately, you don't need the captor at all – you already know the value which is passed to your method and that value is person.

So you can get rid of the ArgumentCaptor (you are not using it anyway) simply verify your call with the real person object reference (because this is what you actually want to verify: "has sendPerson been called with the correct person?"):

verify(personClient).sendPerson(person);

I also note that you have .thenReturn(Response.class) in your stub, which doesn't make any sense. You want a Response object (e.g. new Response() which is of type Response), not the class metadata of the Response class (i.e. Response.class which is of type Class<Response>). I'm surprised that this even compiles, but I don't know the signature of your sendPerson method; perhaps it returns Object or <T> T, so the compiler does not complain. Nevertheless, the way you set up your stub is definitely incorrect.

Related, but not an exact dupe: Why are my mocked methods not calld when executing a unit test?

PS. There are lots of misconceptions in your question on how to use Mockito. I would suggest to take a step back and start with easy and simple tests, without any third party frameworks. Use good old dependency injection, manually create and inject test doubles, assert the result state. Once you have a better understanding, switch parts to Mockito (but often, tests can be written without requiring Mockito).

knittl
  • 246,190
  • 53
  • 318
  • 364
  • now I am getting this error for Runnable: bad return type in lambda expression: void cannot be converted to Object – user12601540 Apr 13 '23 at 07:42
  • @user12601540 sorry, I forgot to actually return something from the stub. The type of the lambda is probably not `Runnable`, but rather a `Callable` or `Supplier`. – knittl Apr 13 '23 at 07:51
  • Updated question, added configuration bean for circuit breaker, but still getting fail for verify() – user12601540 Apr 13 '23 at 09:11
  • @user12601540 The circuit breaker bean is not used, because you have `@Mock CircuitBreaker circuitBreaker` and `@InjectMocks CustomerServiceImpl customerService` in your test. – knittl Apr 13 '23 at 09:40
  • @user12601540 where have you added the `when`? Can you debug your code and check if the mock is used? Have you read the related question which I have linked at the end of my answer? – knittl Apr 13 '23 at 09:41
  • I debug my code, circuitBreaker is mocked, but the result of personClient.sendPerson(person) is null, so when circuitBreaker.run() implements, it returns null – user12601540 Apr 13 '23 at 10:00
  • @user12601540 but you don't assert the return value of sendPerson, you only verify if it has been called. – knittl Apr 13 '23 at 11:05