junit5 icon indicating copy to clipboard operation
junit5 copied to clipboard

Add support for resolving Kotlin inline value class parameters

Open TWiStErRob opened this issue 4 months ago • 9 comments

Note: this might generalize to Kotlin value classes?

Steps to reproduce

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.MethodSource

class ResultTest {
    /**
     * This test passes.
     * Good pass: .getOrThrow() returns the expected type and value.
     */
    @Test
    fun normal() {
        val result: Result<String> = Result.success("something")
        val actual = result.getOrThrow()
        assertEquals("something", actual)
    }

    /**
     * This test passes.
     * Good pass: the cast is invalid and therefore .getOrThrow() should throw as it does.
     */
    @Test
    fun cast() {
        val result: Result<String> = Result.success("something")

        @Suppress("UNCHECKED_CAST")
        val castResult = result as Result<Result<String>>
        assertThrows<ClassCastException> {
            val actual = castResult.getOrThrow()
        }
    }

    /**
     * This test passes.
     * Good pass: direct calling the method returns the right type.
     * This to me proves that the issue is somewhere inside @ParameterizedTest handling.
     */
    @Test
    fun direct() {
        val args = valueProviderFull()
        @Suppress("UNCHECKED_CAST")
        val result: Result<String> = args.single().get().single() as Result<String>
        val actual = result.getOrThrow()
        assertEquals("something", actual)
    }

    /**
     * This test passes.
     * Good pass: the type of the parameter matches the type of the value provided as the argument from method source.
     */
    @MethodSource("valueProviderRaw")
    @ParameterizedTest
    fun parameterizedRaw(value: String) {
        val result: Result<String> = Result.success(value)
        val actual = result.getOrThrow()
        assertEquals("something", actual)
    }

    /**
     * This test errors with:
     * > java.lang.ClassCastException: class kotlin.Result cannot be cast to class java.lang.String.
     * This test should pass, because the argument from the method source is a Result<String>.
     */
    @MethodSource("valueProviderFull")
    @ParameterizedTest
    fun parameterizedFull(result: Result<String>) {
        val actual = result.getOrThrow()
        assertEquals("something", actual)
    }

    /**
     * This test passes.
     * This test should fail when trying to call `castResult.getOrThrow()`.
     */
    @MethodSource("valueProviderFull")
    @ParameterizedTest
    fun parameterizedFullCast(result: Result<String>) {
        @Suppress("UNCHECKED_CAST")
        val castResult = result as Result<Result<String>>
        val actual = castResult.getOrThrow()
        assertEquals(Result.success("something"), actual)
        assertEquals("something", actual.getOrThrow())
    }

    /**
     * This test passes.
     * This test should fail when trying to call `result.getOrThrow()`,
     * because the provided argument is a Result<String>.
     */
    @MethodSource("valueProviderFull")
    @ParameterizedTest
    fun parameterizedFullMistyped(result: Result<Result<String>>) {
        val actual = result.getOrThrow()
        assertEquals(Result.success("something"), actual)
        assertEquals("something", actual.getOrThrow())
    }

    companion object {
        @JvmStatic
        fun valueProviderRaw() = listOf(
            Arguments.of("something"),
        )

        @JvmStatic
        fun valueProviderFull() = listOf(
            Arguments.of(Result.success("something")),
        )
    }
}

Context

  • Used versions (Jupiter/Vintage/Platform): 5.12.0, 5.13.0, 5.14.0, 6.0.0
  • Build Tool/IDE: Gradle
  • JVM: 17, 21
  • Kotlin: 2.0.0, 2.1.0, 2.2.20

Deliverables

  • [ ] Tests should fail/pass as indicated in comments.

TWiStErRob avatar Oct 21 '25 17:10 TWiStErRob

Same applies to @ArgumentsSource(MyArgumentsProvider::class)

class MyArgumentsProvider : ArgumentsProvider {
    override fun provideArguments(context: ExtensionContext) = Stream.of(
        Arguments.of(Result.success("something")),
    )
}

TWiStErRob avatar Oct 21 '25 17:10 TWiStErRob

Note: this might generalize to Kotlin value classes?

Or inline value classes?

marcphilipp avatar Oct 22 '25 07:10 marcphilipp

Yes those. The inline part is implied, because:

e: [VALUE_CLASS_WITHOUT_JVM_INLINE_ANNOTATION] Value classes without '@JvmInline' annotation are not yet supported.

But also, the generics might be important here. Didn't try with a custom value class yet.

TWiStErRob avatar Oct 22 '25 08:10 TWiStErRob

While I can understand the expectation that using Kotlin (inline) value classes should "just work", judging by similar similar issues in other projects (e.g. https://github.com/spring-projects/spring-framework/issues/31698), adding support for them is not trivial.

After some preliminary research, it seems we can get it to work when calling test methods having inline value class parameters via kotlin-reflect which is currently an optional dependency.

POC: https://github.com/junit-team/junit-framework/tree/marc/5081-kotlin-value-class-support

@TWiStErRob Would you be interested in working on this? It definitely needs more tests, including using inline value class parameters and return types in Java etc.

marcphilipp avatar Oct 23 '25 08:10 marcphilipp

Hi! I’d like to work on this issue. Could you please assign it to me? @marcphilipp

Ogu1208 avatar Nov 02 '25 12:11 Ogu1208

@Ogu1208 Sure! Please let us know if you have any questions or want to discuss anything.

marcphilipp avatar Nov 02 '25 14:11 marcphilipp

@marcphilipp id like to work on this issue if @Ogu1208 isnt working on it.

@rishabhjain1712

rishabhjain1712 avatar Nov 08 '25 10:11 rishabhjain1712

@Ogu1208 Are you working on it?

marcphilipp avatar Nov 08 '25 11:11 marcphilipp

@marcphilipp @rishabhjain1712 Sorry for the delay! I'm currently working on this issue and will submit a PR soon.

Ogu1208 avatar Nov 08 '25 12:11 Ogu1208