2

Mockito doesn't seem to be able to spy on function calls inside Functional interfaces. Suppose I have a simple Spring Boot App with a service:

@Service
public class TestService {

    Function<Integer, Integer> mapping = this::add2;

    Integer add2(Integer integer) {
        return integer + 2;
     }

}

And a test:

@SpyBean
TestService testService;

@Test
public void mockitoTest2(){

    doReturn(6).when(testService).add2(2);

    System.out.println(testService.mapping.apply(2));

}

The test will return 4 instead of 6. Is this expected, or worth a bug report?

PeMa
  • 1,559
  • 18
  • 44

1 Answers1

2

This is expected. Mockito creates a spy by making a shallow copy, and the method reference this::add2 gets copied over while keeping the reference to the old this.

TestService myTestService = new TestService();
TestService mySpy = Mockito.spy(myTestService);

In this example, mySpy is an instance of a generated subclass of TestService, which has all of its overridable methods overridden to delegate to Mockito, and all of its instance state shallow-copied from myTestService. This means that myTestService.mapping == mySpy.mapping, which also implies that the reference to this (meaning myTestService) is captured in the Function is copied over.

Method references applied to an instance capture that instance, as on the Oracle page on Method References under "Kinds of Method References". The object that receives the add2 call is the original, not the spy, so you get the original behavior (4) and not the spy-influenced behavior (6).

This should be somewhat intuitive: You can call the Function<Integer, Integer> without passing around a TestService instance, so it's pretty reasonable that the Function contains an implicit reference to a TestService implementation. You're seeing this behavior because the spy instance has its state copied from a real instance after the Function is initialized and this is stored.


Consider this alternative, which you could define on TestService:

BiFunction<TestService, Integer, Integer> mapping2 = TestService::add2;

Here, the function mapping2 doesn't apply to a particular object, but instead applies to any instance of TestService passed in. Consequently, your test would call this:

@Test
public void mockitoTest2(){
    doReturn(6).when(testService).add2(2);
    System.out.println(testService.mapping2.apply(testService, 2));
}

...and because you are passing in your spy testService, it will handle the virtual method call to add2 and invoke the behavior set on the spy (returning 6). There is no implicitly-saved this, so your function works as you'd expect.

See also: Mockito runnable: wanted but not invoked?

Jeff Bowman
  • 90,959
  • 16
  • 217
  • 251
  • OK, almost clear. I just wonder, what is the difference to `mapping` being a normal function, where apparently the reference to `this` doesn't get copied over. And is there another way around that, but keeping the reference to the service class in the function? In my original case, I have this function as part of an `.andThen()` chain, where both workarounds would cause the code to be way less readable. – PeMa Sep 18 '18 at 06:05
  • 1
    Somehow argument saying *You can call the Function without passing around a TestService instance, so it's pretty reasonable that the Function contains an implicit reference to a TestService implementation* does not quite gets to me as it is not in contrary of swapping `this` in lambda. If it would be swapped, function still could be called anywhere without providing invocation target. Any resources (without reading code) on how actually Mockito is creating a spy (i always thought it creates proxy). Im not arguing here - just trying to understand the deep cause of this. – Antoniossss Sep 18 '18 at 06:47