mockito-kotlin
mockito-kotlin copied to clipboard
Mocking a function with matchers that has default arguments throws InvalidUseOfMatchersException
class UnderTest {
fun doStuff(string1: String, string2: String = "hello world") = "do stuff"
}
@Test
fun testIt() {
val underTest = mock(UnderTest::class.java)
whenever(underTest.doStuff(anyString())).thenReturn("stuff done")
}
InvalidUseOfMatchersException "This exception may occur if matchers are combined with raw values"
Theoretically adding @JvmOverloads to doStuff should fix it but it didn't. Even if it did, it would be nice if mockito-kotlin could figure out how to generate a matcher that matches the default value. Alternatively, automatically generate an any() matcher.
I have the @JvmOverloads
, but the method is called from java code, it's not working 🤔
Work around:
I created a Java class with static methods to do my verify
calls when I have default arg, and call this from my kotlin test file.
Not sure it'll work in your case though, in mine the issue is probably the fact that the real code is call from a java code, but my test is in kotlin.
To solve this issue you can try the following code:
class UnderTest {
fun doStuff(string1: String, string2: String) = "do stuff"
fun doStuff(string1: String) = doStuff(string1, "hello world")
}
I know it's not the true Kotlin way, but it's only a workaround.
The thing here is that you're never actually invoking underTest.doStuff(String)
. The Kotlin compiler doesn't generate actual method overloads, but rather a static function with a bunch of helper variables:
public class UnderTest {
@NotNull
public final String doStuff(@NotNull String string1, @NotNull String string2) {
Intrinsics.checkParameterIsNotNull(string1, "string1");
Intrinsics.checkParameterIsNotNull(string2, "string2");
return "do stuff";
}
// $FF: synthetic method
@NotNull
public static String doStuff$default(UnderTest var0, String var1, String var2, int var3, Object var4) {
if (var4 != null) {
throw new UnsupportedOperationException("Super calls with default arguments not supported in this target, function: doStuff");
} else {
if ((var3 & 2) != 0) {
var2 = "hello world";
}
return var0.doStuff(var1, var2);
}
}
}
The invocation underTest.doStuff("foo")
happens as follows:
UnderTest.doStuff$default(underTest, "foo", (String)null, 2, (Object)null);
..and eventually will call the original two-parameter method. In the test case, this leads to one matcher parameter and one non-matcher parameter.
In the end, functions with default values are just syntactic sugar. If you want to verify that these methods are invoked properly, include the entire argument list.
I wonder if it would be possible to use the advanced Mockito APIs to intercept the call and add the extra matchers automatically. https://static.javadoc.io/org.mockito/mockito-core/2.23.4/org/mockito/Mockito.html#framework_integrations_api
I wonder if it would be possible to use the advanced Mockito APIs to intercept the call and add the extra matchers automatically.
just my un-educated guess: it might depend on the way the code generation deals with default parameters. if they are evaluated early and then dropped instead of being added to some meta-structure ... its lower in chances that the default value will still be available during the parsing/resolving of the mock creation and verify statements. but i suppose they are there because any other parsing/resolve of a regular invocation would be able to access those parameter defaults. thus i have to assume they are present at least during most of the parsing stages. (if that were the creation of some c/c++ object file i would guess the exported symbol names in neither the raw nor the mangled name version would have any default bound to it - but that's a pretty late level.)
I understand the technical limitations, but it becomes a bit tricky if you have this class to mock
class ToBeMocked(private val stuffProvider: () -> String) {
fun doSomething(foo: Foo, stuff: String = stuffProvider()) { ... }
}
Because then, there is no proper way to mock, this method when used with only 1 param: the stuffProvider()
will generate a NullPointerException. I had to create an additional fun doSomething(foo: Foo) = doSomething(foo, stuffProvider())
and this is actually duplicating the default value, sad to write.
It could be great to write another line of mocking in the setup to "rewrite"/bypass the generated static method.