Mockito: Verifying overloaded methods with type-co

2019-04-10 05:57发布

问题:

Consider you want to mock an interface using Mockito containing the following method signatures:

public void doThis(Object o);

public void doThis(Object... o)

I need to verify that doThis(Object o) (and not the other method) has been invoked exactly one time.

First I thought that the following line would do the trick:

verify(mock, times(1)).doThis(anyObject());

However, as this seems to work on Windows, it doesn't on Linux because in this environment, a call to of the other doThis method is expected.
This is caused because the anyObject() argument seems to match both method signatures and one is chosen in a more or less unpredictable way.

How can I enforce that Mockito always chooses doThis(Object o) for verification?

回答1:

This is not a mockito issue.

During further investigating, I realized that the actual method is chosen at compile-time (JLS §15.12.2). So basically the class files differed between windows and linux which caused different mockito behavior.

The interface is discouraged (see Effective Java, 2nd Edition, Item 42). I changed it to match the following:

public void doThis(Object o);

public void doThis(Object firstObj, Object... others)

With this change the expected (first) method will always be chosen.

One thing is still left:
Why does the java compiler on windows (eclipse compiler) produce a different output than Oracle JDK javac on Linux?

This might be a bug in ECJ because I would expect that the java language spec is pretty strict here.



回答2:

I agree to most from the other answer, just one part hasn't been answered yet: why the difference in compilers?

At a closer look this is not a difference between ecj and javac, but between different versions of JLS:

  • compiling at compliance 1.7 or less, both compilers select the first (single argument) method.
  • compiling at compliance 1.8, both compilers select the second (varargs) method

Let's have a look at the produced bytecodes:

     7: invokestatic  #8                  // Method anyObject:()Ljava/lang/Object;
    10: checkcast     #9                  // class "[Ljava/lang/Object;"
    13: invokevirtual #10                 // Method doThis:([Ljava/lang/Object;)V

(both compilers also agree on these bytes)

This is to say: inference in Java 8 has become more powerful and is now able to infer the type argument for anyObject() to Object[]. This can be seen by the checkcast instruction, which translated back into source code reads: (Object[])anyObject(). This implies that also doThis(Object...) can be invoked without varargs magic, but by passing a single argument of type Object[].

Now both methods are applicable in the same category ("applicable by fixed arity invocation") and search for the most specific among applicable methods selects the second method.

By contrast, Java 7 would allow invoking the second method only as a variable-arity invocation, but this is not even tried if a fixed-arity match is found.

The above also indicates how to make the program robust against the changes in JLS:

verify(mock, times(1)).doThis(Matchers.<Object>anyObject());

This will tell all compilers at all versions to select the first method, because now it will always see the argument of doThis() as Object -- if you really can't avoid this unhealthy overload, that is :)