I'm trying to understand how mocks in Mockito work. Here is a simple example.
My questions are:
set
of the class Container
and the private variable set
of the class ContainerTest
refer to the same object in memory?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?
}
}
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:
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.
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:
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.
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: