Skip to content

Conversation

@m-koops
Copy link
Contributor

@m-koops m-koops commented Dec 12, 2025

This PR is a direct follow-up to #557 , in which @jselbo introduced some test cases that proved Mockito-Kotlin to fail on handling (nullable) primitive value types in eq matcher and argument captor.

Some background: Kotlin compiler will typically pass on value classes unboxed to Java calls. While unboxing, it will apply the nullability of the value class to the unboxed inner type. But with one exception: when the inner type is a Java primitive then nullability cannot be applied to that primitive. In that case Kotlin compiler passes on the unboxed value type into Java calls.
This exceptional behavior is what is breaking MOckito-Kotlin.

To fully cover handling of (nullable) primitive value class types, tests have been added/activated for those types in eq and any matchers, in argument captors and as return values in synchronous and suspendable functions.

Additionally, the boxing and unboxing logic has been extracted to a new class ValueClassSupport, to facilitate for reuse of the logic and to test the logic with dedicated unit tests.

@m-koops m-koops force-pushed the 555-primitive-value-classes branch from 6ae5106 to bb0c67a Compare December 12, 2025 16:07
@m-koops m-koops changed the title 555 primitive value classes Follow-up on primitive value classes Dec 12, 2025
@jselbo
Copy link
Contributor

jselbo commented Dec 12, 2025

Thanks! This is a great improvement. I ran our tests against this and it fixed several issues.

I found two remaining issues.

  1. Use of suspend functions in argumentCaptor breaks

example:

val callbackCaptor = argumentCaptor<suspend () -> Unit>()

fails to build with:

error: suspend functional types are not supported in typeOf

I tried using typealias to define the function but this fails with the same error

  1. eq() on nullable primitive value class only works if casted

In this test case you added as PrimitiveValueClass? and the test fails without the cast, which isn't intuitive

        fun eqNullablePrimitiveValueClass() {
            val primitiveValueClass = PrimitiveValueClass(123) as PrimitiveValueClass?

I can work around in test code by adding the cast but is there a way to fix this to avoid this requirement?

@m-koops
Copy link
Contributor Author

m-koops commented Dec 12, 2025

Many thanks for the feedback, I will dive into both issues and hope to come with improvements.

…ents: let the matcher match with either boxed or unboxed value as actual call argument.
@m-koops
Copy link
Contributor Author

m-koops commented Dec 12, 2025

I was able to find a solution for the 2nd issue you mentioned.

Within the definition of the matchers there is no context of the parameter signatures of the stubbed function call, it should match with. Therefore there is no way to detect the mismatch between a nullable function parameter and a non-nullable argument for the eq matcher function.

I've fixed the mismatch for (nullable) arguments in eqValueClass() by relaxing that matcher: it now matches the actual mock function call argument against either the boxed or unboxed value of eq-matcher's argument.

I will have a look into the captor issue later this weekend.

@m-koops
Copy link
Contributor Author

m-koops commented Dec 13, 2025

The issue with the captors is a nasty one: the typeOf<T>() function, which is used in KArgumentCaptor to detect the runtime nullability of the type parameter(s) in the various overloads of factory methods argumentCaptor(), is not compatible with suspend functions as type parameter. It doesn't even matter if that typeOf() call is actually on a reachable code branch in the function, the compiler just fails over the incompatibility of the reified type parameter in case of a suspend function.

I have introduced a new factory method suspendFunctionArgumentCaptor() that can deal well with the suspend function type argument (which in this case is declared non-nullable). This new method can be applied as second-best alternative for the broken use of argumentCaptor(). Sorry, I did my very best to find a smart backwards compatible solution but there isn't (yet). The incompatibility in typeOf<T>() is descriped in Kotlin issue KT-47562. In future, when this ticket is solved, suspendFunctionArgumentCaptor() can be dropped in favor of the generic factory method argumentCaptor().

At least, the use of ArgumentCator with (suspend) funtion arguments is now covered with tests.

For me this completes this PR, I hope you agree with me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants