Search code examples
javaunit-testingmockingmockito

Are Mockito @Mock variables references to variables of class under test?


I'm trying to understand how mocks in Mockito work. Here is a simple example.

My questions are:

  • Do the private variable set of the class Container and the private variable set of the class ContainerTest refer to the same object in memory?
  • How can I test the two methods add() and remove()? I have a few lines below and an assertion that fails.
public class Container
{
    private Set<Object> set;

    public Container()
    {
        set = new HashSet<>();
    }

    public add(Object o)
    {
        set.add(o);
    }

    public remove(Object a)
    {
        set.remove(o);
    }
}
public class ContainerTest
{
    @Mock
    private Set<Object> set;

    @InjectMocks
    private Container container;

    @Test
    public void testAdd()
    {
        container.add(mock(Object));
        // Assertion fails
        assertTrue(set.contains(any(Object)));
    }

    @Test
    public void testRemove()
    {
        // what do I do here?
    }
}

Solution

  • There's a lot going on in your question. Let's fix the compilation errors first, then dissect and discuss your example.

    The following compiles (and follows most Java formatting guidelines so it doesn't look like C#):

    public class Container {
        private Set<Object> set;
    
        public Container() {
            set = new HashSet<>();
        }
    
        public void add(Object o) {
            set.add(o);
        }
    
        public void remove(Object o) {
            set.remove(o);
        }
    }
    
    public class ContainerTest {
        @Mock
        private Set<Object> set;
    
        @InjectMocks
        private Container container;
    
        @Test
        public void testAdd() {
            container.add(mock(Object.class));
            // Assertion fails
            assertTrue(set.contains(any(Object.class)));
        }
    
        // public void testRemove() …
    }
    

    To answer your first question (but please keep in mind that a Stackoverflow question should generally only contain a single question):

    No, ContainerTest#set and Container#set are not the same instance – but not for the reason you think. Your test is missing the @ExtendWith annotation and all your annotations are useless. Execute the test with the appropriate extension and both fields will in fact reference the same instance:

    @ExtendWith(MockitoExtension.class)
    public class ContainerTest {
    

    (The general warnings apply: don't mock a type you don't own, don't mock value objects (e.g. collections), don't mock everything).

    That out of the way, we can have a look why your test fails. First, you should ask yourself which observable behavior of your class/unit you want to test. Your code as shown doesn't implement anything useful (sorry): you have a container and you can add and remove things, but you cannot test if it contains something. How would real production code use this class? All state is private, not a single method returns something, and there are zero (external) collaborators.

    Anyway … let's look at your test method:

    @Test
    public void testAdd() {
        container.add(mock(Object.class));
        assertTrue(set.contains(any(Object.class)));
    }
    

    What does it do:

    1. It calls add on your container with a new Mockito mock instance. Since you are only using Object, you don't really need a mock and new Object() would work as well.

    2. It calls contains on the set mock instance. This method is going to return false, every time. Why? It's called on a Mockito mock instance and these do nothing by default. They only return the values that you have stubbed with when/thenReturn. Mockito mocks do not have state nor exhibit any behavior.

      But even if set had contains properly stubbed or implemented, your assertion would still fail. Why? any(Object.class) pushes a matcher onto the matcher stack, then returns null. So your assertion effectively reads assertTrue(set.contains(null));.

      (set.contains(any(Object.class)) sounds like nice and proper English, but it won't work in your test – you can only call a method on an instance with concrete arguments. Argument matchers can only be used as part of a verify or when call)

    So there are two approaches how to actually implement your test:

    1. State verification: add something in your container, then assert that the set contains the same (or equal) object.

      @Test
      public void testAdd() {
          Object obj = new Object();
          container.add(obj);
          assertTrue(set.contains(obj));
      }
      

      Unfortunately, this will not work when set is a Mock (because Mockito mocks do not have any inherent behavior. So either use a real collection or a Mockito Spy.

    2. Behavior verification: add something to your container, then verify that a method on a collaborator was called with the correct arguments:

       @Test
       public void testAdd() {
           Object obj = new Object();
           container.add(obj);
           verify(set).add(obj); // or: verify(set).add(eq(obj));
       }
      

      Alternatively, use a different matcher that verifies calls based on certain criteria:

       @Test
       public void testAdd() {
           Object obj = new Object();
           container.add(obj);
           verify(set).add(any(Object.class));
       }
      

      This will pass for any object, so an implementation such as void add(Object o) { Object differentObject = new Object(); set.add(differentObject); } would still make the test pass, but it all depends on what you are actually trying to verify.

    Similarly, for your testRemove, you could either implement a state-based or a behavior-based test:

    @Test
    public void testRemoveStateBased() {
        Object obj = new Object();
        container.add(obj);
        assumeTrue(set.contains(obj)); // optional
        container.remove(obj);
        assertFalse(set.contains(obj));
    }
    
    @Test
    public void testRemoveBehaviorBased() {
        Object obj = new Object();
        container.remove(obj);
        verify(set).remove(obj); // or: verify(set).remove(eq(obj));
    }
    

    Further reading: