mockito / mockito-kotlin

Using Mockito with Kotlin
MIT License
3.09k stars 198 forks source link

Vararg verification failures using mockito 4.11.0 #473

Closed ZOlbrys closed 2 months ago

ZOlbrys commented 1 year ago

Consider the following code:

package com.xyz.packagexyz

open class TestService {
    fun doSomethingElse(message: String, vararg args: Any) {
        print("message: $message, args: $args")
    }
}

class TestClass(val service: TestService) {
    fun doSomething(message: String, vararg args: Any) {
        service.doSomethingElse(message, args)
    }
}

With code testing this:

internal class TestClassTest {
    private val mockService = mock<TestService>()
    private val testClass = TestClass(mockService)

    @Test
    fun test() {
        val message = "message"
        val arg1 = "arg1"
        testClass.doSomething(message, arg1)

        verify(mockService).doSomethingElse(message, arrayOf(arg1))
    }
}

Using mockito 4.11.0/mockito-kotlin 4.1.0, the test here fails with

Argument(s) are different! Wanted:
testService.doSomethingElse(
    "message",
    ["arg1"]
);
-> at com.xyz.packagexyz.TestService.doSomethingElse(TestClass.kt:5)
Actual invocations have different arguments:
testService.doSomethingElse(
    "message",
    ["arg1"]
);
-> at com.xyz.packagexyz.TestClass.doSomething(TestClass.kt:11)

Using mockito 4.10.0, the test above passes successfully. Any ideas?

This was also asked on stackoverflow which includes some workarounds that IMO do not make sense to use (I feel like a bug is happening with the code above), but maybe I am mistaken.

After discussion on https://github.com/mockito/mockito/issues/2856 it was requested to move the bug here. In that issue it was discovered that if the source code under test is written in Java and the verification is done using eq method such as:

verify(mockService).doSomethingElse(eq(message), eq(arg1))

the test passes (using mockito directly and/or with mockit-kotlin). It's only when using kotlin source code that this fails.

TWiStErRob commented 2 months ago

@TimvdLippe I think this works as expected.

The problem is not in Mockito but in user code. There's a difference between what doSomethingElse(message, args) means in Java vs Kotlin.

Here's a repro with 3 classes without using Mockito:

class Tests { // Doesn't matter if test is written in Java or Kotlin
    @Test fun kotlin() {
        TestClassK().doSomething("message", "arg1")
    }
    @Test fun java() {
        TestClassJ().doSomething("message", "arg1")
    }
}

In Java args is an Object[] and passing an Object[] to an Object... is an exact match. Therefore it just passes the args array as reference without creating another vararg Object[].

class TestClassJ {
    private void doSomethingElse(String message, Object... args) {
        System.out.printf("message: %s, args: %s%n", message, Arrays.deepToString(args));
    }
    void doSomething(String message, Object... args) {
        // output: "message: message, args: [arg1]"
        doSomethingElse(message, args);
        // output: "message: message, args: [[arg1]]"
        doSomethingElse(message, (Object) args); // Object is not an array, so will get auto-wrapped to adhere to the `...` method call.
        // output: "message: message, args: [[arg1]]"
        doSomethingElse(message, new Object[] {args}); // Java needs explicit wrapping.
    }
}

In Kotlin args is Array<Any> which happens to be also an Any so the inner vararg happily accepts it (each vararg creates an array, unless you spread the delegating call).

class TestClassK {
    private fun doSomethingElse(message: String, vararg args: Any) {
        println("message: $message, args: ${args.contentDeepToString()}")
    }
    fun doSomething(message: String, vararg args: Any) {
        // output: "message: message, args: [[arg1]]"
        doSomethingElse(message, args)
        // output: "message: message, args: [[arg1]]"
        doSomethingElse(message, args as Any)
        // output: "message: message, args: [arg1]"
        doSomethingElse(message, *args) // Kotlin needs explicitly unwrapping (via spread operator).
    }
}

Note that all this is only relevant with Object.../vararg Any, if you used any other type you would get a compiler error in both languages. Try replacing every Object/Any with String in the above code. Kotlin will force you to spread and Java won't allow you to explicitly wrap.

TWiStErRob commented 2 months ago

@ZOlbrys To explain the diff between 4.10 and 4.11 I think we just need to look at the fixes made in 4.11: https://github.com/mockito/mockito/releases/tag/v4.11.0; based on this I think 4.10 was giving you a false positive and 4.11 is actually highlighting a bug in your code: a missing spread operator:

class TestClass(val service: TestService) {
    fun doSomething(message: String, vararg args: Any) {
-        service.doSomethingElse(message, args)
+        service.doSomethingElse(message, *args)
    }
}

(assuming your test verification expectation is really what you intend the code to do)

TimvdLippe commented 2 months ago

Cool, thanks for confirming @TWiStErRob !

ZOlbrys commented 2 months ago

@TWiStErRob yeah, that makes total sense! I checked my code and this makes sense.

I appreciate the assistance here, thank you!!