3

I have a class Sut with lazy initialization implemented using java.util.function.Supplier. In fact it's more complicated that the code below, but this is the simplest form that Mockito cannot test. The test below throws an error Wanted but not invoked ... However, there were other interactions with this mock. Why doesn't Mockito count the invocation of create? The code flow actually enters create(); I checked that with debugger.

import java.util.function.Supplier;

import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

public class TestTimes {

    @Test
    public void testCreateOnlyOnce() {
        Sut sut = spy(new Sut());
        sut.getData();
        sut.getData();
        sut.getData();
        verify(sut, times(1)).create();
    }

    private static class Sut {
        Supplier<Object> data = this::create;

        void getData() {
            data.get();
        }

        Object create() {
            return new Object();
        }
    }
}
Peter
  • 1,512
  • 1
  • 22
  • 40
  • 1
    I don't know the answer, but I'd just like to say that this is maybe one of the best examples of an [mcve](https://stackoverflow.com/help/minimal-reproducible-example) (minimal, complete and verifiable example) I've ever seen on this site. – yshavit Jun 26 '19 at 21:22

2 Answers2

3

First of all, thanks for the well written question.

I have tested your code myself and seen the error you mentioned. Although, I have changed your code a little bit while debugging... Take a look:

    @Test
    public void testCreateOnlyOnce() {
        Sut sut = spy(new Sut());
        sut.getData();
        sut.getData();
        sut.getData();
        verify(sut, times(1)).create();
    }

    private static class Sut {

        private Supplier<Object> data;

        // Added de data object initialization on the constructor to help debugging.
        public Sut() {
            this.data = this::create;
        }

        void getData() {
            data.get();
        }

        Object create() {
            return new Object();
        }
    }

What I found out while debugging:

  1. The Sut class constructor is being called correctly inside the spy(new Sut()) clause, but the create() method is not being called there.
  2. Every time sut.getData() is called, the create() method is also called. What made me conclude, finally that:

On the constructor, all that this::create did was telling java that, whenever it needs to retrieve the Object from the supplier, that Object will be retrieved from the create() method. And, the create() method being called by the supplier is from a class instance different from what Mockito is spying.

That explains why you cannot track it with verify.

EDIT: From my research, that is actually the desired behavior of the Supplier. It just creates an interface that has a get() method that calls whatever noArgs method you declared on the method reference. Take a look at this on "Instantiate Supplier Using Method Reference".

Gabriel Robaina
  • 709
  • 9
  • 24
3

I want to add a little to Gabriel Pimentas excellent answer. The reason why this works as it does is that mockito creates shallow copies of the spy new Sut() and your Supplier refers to the compiled lambda method inside the original Sut instance and not the spy instance.

See also this question and the mockito documentation.

When you debug your code, you can see how this works:

Sut sut = spy(new Sut()); is now a mocked/spied subclass of Sut as the instance TestTimes$Sut$MockitoMock$1381634547@5b202a3a. Now, all fields from the original new Sut() are shallow-copied, including the Supplier<Object> data. Looking at this field inside the spy, we can see that it is an instance of TestTimes$Sut$$Lambda$1/510109769@1ecee32c, i.e. pointing to a lambda inside the original Sut. When we set a breakpoint inside the create method, we can further observe that this points to TestTimes$Sut@232a7d73, i.e. the original Sut and not the spied instance.

EDIT: Even though this MCVE probably does not resemble your actual code, the following solutions come to mind:

  • Inject the Supplier into your Sut (either during construction or as a parameter to getData.
  • Create the supplier lazily within your getData method (so that it points to the mockito-instance)
  • Don't use a Supplier but just call create directly if the Supplier is not passed from the outside
sfiss
  • 2,119
  • 13
  • 19