43

I want to test the following method:

    public void dispatchMessage(MessageHandler handler, String argument1, String argument2, Long argument3) {

    handler.registerMessage(() -> {
        dispatcher.dispatch(argument1,
                argument2,
                argument3);
    });

}

Where MessageHandler is a helper class which will accept a Functional Interface implementation in the form a lambda, and store it for later execution.

Is there a way to verify with mockito that the dispatchMessage method of the mocked MessageHandler has been called with the specific lambda expression:

Meaning, can I write such a test:

        @Test
public void testDispatchMessage_Success() throws Exception {

    myMessageDispatcher.dispatchMessage(handler, "activityId", "ctxId", 1l, );

    verify(handler, times(1)).dispatchMessage(() -> {
        dispatcher
            .dispatch("activityId", "ctxId", 1l,);
    });

}

}

This test will result in assertion error: Argument(s) are different! Wanted:

......Tests$$Lambda$28/379645464@48f278eb

Actual invocation has different arguments:

..........Lambda$27/482052083@2f217633

which makes sense since mockito tries to compare two different implementations of the functional interface, which have a different hash code.

So is there some other way to verify that the method dispatchMessage() has been called with a lambda that returns void and has a body method of dispatcher.dispatch("activityId", "ctxId", 1l,); ?

abpatil
  • 916
  • 16
  • 20
JavaSloth
  • 435
  • 1
  • 4
  • 10

3 Answers3

59

Yes, you can. The trick here is that you have to get to the instance of the lambda that is passed to the registerMessage and then execute that expression and then you can verify the result.

For the purpose of a meaningful example I created this Handler class that contains the dispatchMessage that you want to test:

public class Handler {

    private Dispatcher dispatcher = new Dispatcher();

    public void dispatchMessage(MessageHandler handler, String argument1, String argument2, Long argument3) {

        handler.registerMessage(() -> {
            dispatcher.dispatch(argument1,
                    argument2,
                    argument3);
        });

    }

    interface MessageHandler {
        void registerMessage(Runnable run);
    }

    static class Dispatcher {
        void dispatch(String a, String b, long c){
            // Do dispatch
        }
    }
}

What you have to remember is that a lambda expression is just a short hand form to pass a function to a method. In this example the function is the run method of a Runnable. Therefore the method registerMessage of the interface for MessageHandler takes a Runnable as it's argument. I also included an implementation for the Dispatcher, which is called from within registerMessage. The test for this looks like this:

@RunWith(MockitoJUnitRunner.class)
public class HandlerTest {
    @Mock
    private Dispatcher dispatcher;
    @InjectMocks
    private Handler classUnderTest;
    @Captor
    private ArgumentCaptor<Runnable> registerMessageLambdaCaptor;

    @Test
    public void shouldCallDispatchMethod() {
        final String a = "foo";
        final String b = "bar";
        final long c = 42L;

        MessageHandler handler = mock(MessageHandler.class);

        classUnderTest.dispatchMessage(handler, a, b, c);

        verify(handler).registerMessage(registerMessageLambdaCaptor.capture());

        Runnable lambda = registerMessageLambdaCaptor.getValue();

        lambda.run();

        verify(dispatcher).dispatch(a, b, c);
    }
}

There is an ArgumentCaptor for the lambda expression which we use in the first verification of the registerMessage. After that verification we can retrieve the lambda expression from the captor. The type of the lambda expression is Runnable, as defined in the MessageHandler interface. Hence we can call the run method on it and then verify that the dispatch method on the Dispatcher was called with all the appropriate arguments.

hotzst
  • 7,238
  • 9
  • 41
  • 64
  • 3
    This is exactly what I had in mind. Thank you for your time. – JavaSloth Jan 04 '17 at 15:16
  • 1
    Same situation, but it says that arguments are different, although it expects a Runnable and the tested code passes a Lambda. – Tudor Sep 18 '19 at 12:09
  • Had same scenario, and have lot of Function based code, this helps a lot solve my mock test – Yoga Gowda May 04 '20 at 21:05
  • 1
    One of the best answer found for lambda invocation testing !! Thanks a lot. – Shridutt Kothari Oct 12 '20 at 17:31
  • 1
    In my case the Runnable did not work (test file was kotlin), but rather i needed Function1 as a type for the capture, because my function took an argument and returned one. – AgentKnopf Nov 30 '20 at 11:45
  • unfortunately, this doesn't solve the problem presented in the description. This solution will verify if method dispatch was ultimately invoked, but it won't tell you that was the argument passed to handler.registerMessage. I know that, due to the nature of this code in particular, there is no other outcome (dispatch was invoked because it was passed as parameter), but there are other scenarios, like the one I'm dealing with right now, where you need to verify that a specific lambda was passed as parameter without necessarily being invoked – Yego May 25 '21 at 16:58
1

You want to evaluate lambda argThat(a -> a.get() and only then compare it's value:

when(dogObject.barkAt(argThat(a -> a.get().equals(PEOPLE_TO_BARK_AT)))).thenReturn(OUTCOME);
Benas
  • 2,106
  • 2
  • 39
  • 66
-1

Outside of the specific lambda, you can verify that your method was called with any lambda expression was called like this:

    verify(handler).registerMessage(any())

    private fun <T> any(): T {
        Mockito.any<T>()
        return null as T
    }
Blundell
  • 75,855
  • 30
  • 208
  • 233
  • That would only check if it was called, not catch any mapping error. Most likely you don't want to send null, null, null as arguments. any() would not catch that. – DannyThunder Jun 10 '21 at 06:56
  • What if there is method overloading? This will not able to find out which method we are actually referring to – Rajan jha Oct 11 '22 at 09:53