andyb
andyb

Reputation: 43823

@InjectMocks behaving differently with Java 6 and 7

With a very simple Mockito run JUnit test and class I am seeing different output when the test is run with Java 1.6.0_32 and Java 1.7.0_04 and want to understand why this is happening. I suspect there is some type erasure going on but would like a definitive answer.

Here is my example code and instructions on how to run from the command line:

FooServiceTest.java

import org.junit.*;
import org.junit.runner.*;
import org.mockito.*;
import org.mockito.runners.MockitoJUnitRunner;
import static org.mockito.Mockito.*;
import java.util.*;

@RunWith(MockitoJUnitRunner.class)
public class FooServiceTest {
  @Mock Map<String, String> mockStringString;
  @Mock Map<String, Integer> mockStringInteger;

  @InjectMocks FooService fooService;

  public static void main(String[] args) {
    new JUnitCore().run(FooServiceTest.class);
  }

  @Before
  public void setup() {
    MockitoAnnotations.initMocks(this);
  }

  @Test
  public void checkInjection() {
    when(mockStringString.get("foo")).thenReturn("bar");
    fooService.println();
  }
}

FooService.java

import java.util.*;

public class FooService {
  private Map<String, String> stringString = new HashMap<String, String>();
  private Map<String, Integer> stringInteger = new HashMap<String, Integer>();

  public void println() {
    System.out.println(stringString.get("foo") + " " + stringInteger);
  }
}

To compile and run this example:

I believe the output from above is null {} because @InjectMocks field injection cannot correctly resolve the types since they are both of type Map. Is this correct?

Now changing one of the mock names to match the field in the class should allow Mockito to find a match. For example changing

@Mock Map<String, Integer> mockStringInteger;

to

@Mock Map<String, Integer> stringInteger;

then compiling/running with Java 1.6.0_32 gives (IMHO the expected) output bar stringInteger but with 1.7.0_04 gives null stringInteger.

Here is how I am running it (from a command line in Windows 7):

E:\src\mockito-test>set PATH="C:\Program Files (x86)\Java\jdk1.6.0_32\bin"
E:\src\mockito-test>javac -cp junit-4.10.jar;mockito-all-1.9.0.jar *.java
E:\src\mockito-test>java -cp .;junit-4.10.jar;mockito-all-1.9.0.jar FooServiceTest
    bar stringInteger
E:\src\mockito-test>set PATH="C:\Program Files (x86)\Java\jdk1.7.0_04\bin"
E:\src\mockito-test>javac -cp junit-4.10.jar;mockito-all-1.9.0.jar *.java
E:\src\mockito-test>java -cp .;junit-4.10.jar;mockito-all-1.9.0.jar FooServiceTest
    null stringInteger

Upvotes: 11

Views: 5663

Answers (3)

bric3
bric3

Reputation: 42233

I believe the output from above is I null {} because @InjectMocks field injection cannot correctly resolve the types since they are both of type Map. Is this correct?

Yes, correct on these field Mockito cannot clear the ambiguity, so it simply ignores these ambiguous fields.

With a very simple Mockito run JUnit test and class I am seeing different output when the test is run with Java 1.6.0_32 and Java 1.7.0_04 and want to understand why this is happening.

Actually the diferrence resides in a different behavior of Arrays.sort and hence Collections.sort() between JDK 6 and JDK 7. The difference resides in the new algorythm that should perform 20% less swaps. That's probably this swap operation that made things work under JDK6 and JDK7.

If I may you are "looking for trouble" if you rename only one mock field of the fields that have the same type (or same erasure). When mocks cannot be differentiated by type you really should name all your mock field to corresponding field, but the Javadoc does not state that clearly.

Thanks a lot for reporting that odd behavior by the way, I created an issue on Mockito, however for now I won't really solve this issue, but rather ensure the same behavior across JDKs. Solving this situation might need to code a new algorythm while maintaining compatibility, in the mean time you should name all your field mocks accordingly to the fields of tested class.

For now the thing to do will probably be tweaking the comparator with additional comparisons to enforce the same order on JDK6 and JDK7. Plus adding some warning in the Javadoc.

EDIT : Making two passes might solve the problem for most people.

Hope that helps. Thx for spotting the issue.


Also by the way you need either MockitoAnnotations.initMocks(this); or the runner @RunWith(MockitoJUnitRunner.class), using both is not necessary, and might even cause some problems. :)

Upvotes: 5

Dawood ibn Kareem
Dawood ibn Kareem

Reputation: 79848

Mockito's behaviour is undefined, if there's more than one mock that matches one of the fields that is going to be injected. Here, "matches" means it's the right type, ignoring any type parameters - type erasure prevents Mockito from knowing about the type parameters. So in your example, either of the two mocks could be injected into either of the two fields.

The fact that you've managed to observe different behaviour with Java 6 from Java 7 is a bit of a red herring. There is no reason, in either version of Java, to expect Mockito to choose correctly between mockStringString or mockStringInteger, for either one of the two fields that it's injecting.

Upvotes: 2

avandeursen
avandeursen

Reputation: 8668

Is this correct?

Indeed, due to type erasure, Mockito can't see the difference between the various Maps at run time / via reflection, which is giving Mockito a hard time doing the proper injection.

The null response to stringString.get("foo") can have two causes:

  1. stringString properly mocked, but stubbing did not take place (get always return null).
  2. stringString not mocked, so still a HashMap without a value for "foo", so get will return null.

The {} response to the stringInteger variable means it has been initialized (in the class) with an actual (empty) HashMap.

So what your output tells you is that stringInteger is not mocked. What about stringString?

If none of the two @Mocks have names that match any of the fields of the class under test, nothing is mocked. The reason? I suspect it can't decide which field to inject into, so it doesn't do any mocking. You can verify this by displaying both variables, which will both yield {}. This explains your null value.

If one of the @Mocks has a name that matches, and the other has one that doesn't (your modification, one mockedStringString and one stringInteger, i.e., having the same name), what should Mockito do?

What you want it to do is only inject one of them and only in the field with the corresponding name. In your case, you have mockedStringString (you would expect this not to match) and stringInteger (you would expect it to match). Since you stubbed the mockedStringString (!), which won't match, the expected outcome would be null.

In other words, I think the Java 7 response is OK, and the Java 6 one is not OK, for the specific example given.

To see what is going on for the (unexpected) behavior you get for Java 6, try having just one @Mock -- if I properly mock stringString and have no mock for stringInteger, the mock for for stringString is injected into the stringInteger field. In other words, Mockito seems to first figure out that it can inject (given the name), and then injects the mock to one of the matching possibilities (but not necessarily the right one).

Upvotes: 0

Related Questions