I need to test my new, shiny Scala code. Usually I write tests in ScalaTest, but for generating stubs I still use good, old Mockito. What can possibly go wrong? I open a new tab in my editor and start hacking test code.
For the first surprise I don’t have to wait too long.
In my code I use value classes
to represent entity IDs.
For example I use CustomerId
:
case class CustomerId(id: Long) extends AnyVal
When I tried to mock customerId
method (property?):
trait RequestContext {
val customerId: CustomerId
// ...
}
in my test, Mockito started complaining about wrong return types and refused to cooperate:
// inside test
val requestContext = mock(classOf[RequestContext])
/* Fails with WrongTypeOfReturnValue exception:
*
* CustomerId cannot be returned by customerId()
* customerId() should return long
*/
when(requestContext.customerId).thenReturn(CustomerId(123L))
Of course my CustomerId
value class is here to blame…
In Scala, value classes offer type safety of normal classes with
performance of the primitives.
Scala compiler achieves this, simply by replacing the value
class type by the primitive type, wherever possible.
So I grabbed javap
and took a look at the generated bytecode,
and indeed there is a long
there:
target/scala-2.13/classes$ javap RequestContext.class
Compiled from "RequestContext.scala"
public interface RequestContext {
public abstract long customerId();
// ...
}
Mockito uses reflection to figure out which type a given method returns. This gives us little hope for a nice solution. We can only try to hack around the problem, for example this monstrosity works:
val requestContext = mock(classOf[RequestContext])
when(requestContext.customerId.asInstanceOf[Object])
.thenReturn(Long.box(123L))
assert(requestContext.customerId == CustomerId(123L))
We use asInstanceOf[Object]
to fool Scala’s type system
and then return a java.lang.Long
instance.
Yeah it works but it is not nice… But hey, first get the job done, then get it done well. Let’s move on to the next test…
I have to verify that a given method was called. This time I should have more luck, right? So I wrote another test and I ran it:
val requestContext = mock(classOf[RequestContext])
requestContext.setRequestId(RequestId(123L))
// works perfectly
verify(requestContext).setRequestId(RequestId(123L))
// does NOT WORK
verify(requestContext).setRequestId(any())
But I saw the results I was completly perplexed.
Verification with RequestId(123L)
worked but the one with
any()
did not. But what is worse the second verification
thrown NullPointerException
. NPE? Really?
My brain was racing, and after a few seconds a thought strike me: it’s these pesky value classes again!
And I was right! Value classes in Scala cannot be null
!
For example, in Scala REPL:
scala> case class FooId(id: Long) extends AnyVal
defined class FooId
scala> val f: FooId = null.asInstanceOf[FooId]
f: FooId = FooId(0)
scala> f.id
res0: Long = 0
// And more...
scala> var o: Object = null
o: Object = null
scala> o.asInstanceOf[FooId]
java.lang.NullPointerException
... 33 elided
any()
matcher that comes with Mockito has a very simple
implementation:
public static <T> T any() {
return anyObject();
}
@Deprecated
public static <T> T anyObject() {
reportMatcher(Any.ANY);
return null;
}
that always returns null
.
The method that I tried to check, has a signature:
trait RequestContext {
def setRequestId(requestId: RequestId)
// ...
}
and since RequestId
is a value class, the “real” JVM signature is:
public abstract void setRequestId(long);
When I write verify(...).setRequestId(any())
Scala compiler adds instructions that convert the object returned
by any()
(remember generics does not exist on JVM level, so all
these T
s and V
s are just Object
s at runtime) to long
.
And this is the reason why I got NullPointerException
earlier.
In bytecode it looks like this:
19: invokestatic #33 // Method org/mockito/Mockito.verify:(Ljava/lang/Object;)Ljava/lang/Object;
22: checkcast #17 // class RequestContext
25: invokestatic #39 // Method org/mockito/ArgumentMatchers.any:()Ljava/lang/Object;
28: checkcast #41 // class RequestId
31: invokevirtual #45 // Method RequestId.id:()J
34: invokeinterface #29, 3 // InterfaceMethod RequestContext.setRequestId:(J)V
and the NPE is thrown by the instruction at the offset 31
.
Now I understand the problem, but I still want to use any()
matcher.
There must be a trick to make it return a valid RequestId
.
But then I realized that even if I found such a way, I would be
bitten again by WrongTypeOfReturnValue
exception or
something similar, since
the method takes long
not RequestId
. What I really need is
a way to use anyLong()
with setRequestId
. It was a good
enough challenge to
start my evil alter-ego working on some frankensteinian solution.
And I found it, I FOUND IT!, I FOUND IT!!! Ehmm… and here it is:
val requestContext = mock(classOf[RequestContext])
requestContext.setRequestId(RequestId(123L))
// works again
verify(requestContext).setRequestId(RequestId(anyLong()))
verify(requestContext).setRequestId(
RequestId(ArgumentMatchers.eq(123L)))
A perfect combination of beauty and evil…
The trick that I used here is that Mockito does not care,
where the matcher is located, it only cares about the time when it
is called. As long as I call anyLong()
after the call to
verify(...)
and before the call to .setRequestId(...)
,
Mockito will work. Actually we may wrap any matcher in
as many dummy calls as we want, as in a(b(c(d(any()))))
,
only the fact that it was called counts.
It can’t be worse right? Two tests, two hacks…
But only I wrote my third test, I was slapped by the next problem, this time caused by default parameters:
trait SomeTrait {
def method(a: Int, b: Int = 100): Int
}
test("...") {
val someTrait = mock(classOf[SomeTrait])
someTrait.method(3)
/* This call fails with:
* Argument(s) are different! Wanted:
* someTrait.method(3, 100);
* Actual invocations have different arguments:
* someTrait.method(3, 0);
*/
// verify(someTrait).method(3, 100)
// works
verify(someTrait).method(3, 0)
}
WTF? Not again… Another strange problem that forces me to look under the bonnet.
Let’s look at the bytecode using javap -c
:
// bytecode of `someTrait.method(3)`
// (some code skipped here)
// 3 - load first arg onto the stack
10: iconst_3
// Scala Magic(TM), call a method to get the second
// argument's default value and push it onto the stack:
// someTrait.method$default$2()
11: aload_0
12: invokeinterface #27, 1
// finally call the `method` method
17: invokeinterface #31, 3
// (some code skipped here)
So the Scala compiler calls a hidden method, with a name
methodName$default$parameterIndex
on the trait,
to figure out what value should be used as a value
of the default parameter. Wow! I didn’t expect something
like that!
If only I could stub this method$default$2
or something
…oh wait I could:
val someTrait = mock(classOf[SomeTrait])
when(someTrait.method$default$2).thenReturn(100)
someTrait.method(3)
// it works!
verify(someTrait).method(3, 100)
Excellent. It is working perfectly but now I have three tests and three hacks. Surely I am doing something wrong here.
And so I harnessed the power of Google (after spending some time looking at the pictures of stoats; hey they are really cute) and found this gem:
I plug it into my project (it even has a special version for ScalaTest) and suddenly everything started to working as it should be.
Stubbing works:
import org.mockito.ArgumentMatchersSugar._ // from Mockito-Scala
import org.mockito.MockitoSugar._
// Don't import Mockito or ArgumentMatchers
val requestContext = mock[RequestContext]
when(requestContext.customerId).thenReturn(CustomerId(123L))
assert(requestContext.customerId == CustomerId(123L))
…and verification works:
val requestContext = mock[RequestContext]
requestContext.setRequestId(RequestId(123L))
verify(requestContext).setRequestId(eqTo(RequestId(123L)))
verify(requestContext).setRequestId(any[RequestId])
// but this will not work: verify(requestContext).setRequestId(any)
even default parameters work:
val someTrait = mock[SomeTrait]
someTrait.method(3)
verify(someTrait).method(3)
verify(someTrait).method(3, 100)
Yay!
Wanna see some real code? Click here.
References: