Mockito: Spying function calls inside Functional i

2020-07-27 05:11发布

问题:

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?

回答1:

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?