hello everyone, in my current project we have seve...
# mockk
j
hello everyone, in my current project we have several instrumentation tests in different packages, when we run the tests in a single package everything works as expected, but if we run several tests in several packages by running from the parent package we get weird
UninitializedPropertyAccessException
, according the the stacktrace the problem traces back to this
java.lang.AbstractMethodError: abstract method "boolean kotlin.reflect.KClass.isValue()"
which seems to be related to mockK, has anybody come across this issue before?
👍 1
a
.isValue()
might be related to value classes. Are you using value classes? What versions of Kotlin and MockK are you using?
j
we're not using value classes, we noticed the exception is thrown at
io.mockk.core.ValueClassSupport.isValue_safe(ValueClassSupport.kt:64)
, and during our inspection decided to downgrade mockK from
1.13.1
to
1.12.0
and suddently our test suit succeeds completelly, so we're looking into what could've changed between these two versions. Oh and we're using Kotlin
1.7.20
a
hmm okay. Did you try the latest MockK version? Can you share the stacktrace (use ‘create a text snippet’ to attach it)?
j
I can't share the complete stacktrace but this is the section we're focusing on
Copy code
01-27 17:30:59.374 32268 32287 E TestRunner: ----- begin exception -----
01-27 17:30:59.376 32268 32287 E TestRunner: java.lang.AbstractMethodError: abstract method "boolean kotlin.reflect.KClass.isValue()"
01-27 17:30:59.376 32268 32287 E TestRunner: 	at io.mockk.core.ValueClassSupport.isValue_safe(ValueClassSupport.kt:64)
01-27 17:30:59.376 32268 32287 E TestRunner: 	at io.mockk.core.ValueClassSupport.getBoxedClass(ValueClassSupport.kt:33)
01-27 17:30:59.376 32268 32287 E TestRunner: 	at io.mockk.impl.instantiation.JvmMockFactoryHelper.toDescription$mockk(JvmMockFactoryHelper.kt:156)
01-27 17:30:59.376 32268 32287 E TestRunner: 	at io.mockk.impl.instantiation.JvmMockFactoryHelper$mockHandler$1.invocation(JvmMockFactoryHelper.kt:26)
01-27 17:30:59.376 32268 32287 E TestRunner: 	at io.mockk.proxy.android.advice.Advice.handle$lambda-0(Advice.kt:78)
01-27 17:30:59.376 32268 32287 E TestRunner: 	at io.mockk.proxy.android.advice.Advice.$r8$lambda$BJwd2E_KuGTkZCQEA_otcmNp7lY(Unknown Source:0)
01-27 17:30:59.376 32268 32287 E TestRunner: 	at io.mockk.proxy.android.advice.Advice$$ExternalSyntheticLambda0.call(Unknown Source:10)
I haven't tried the latest version but it's not possible to upgrade our current version either
a
ah, Android… I’m not familiar with Android dev. Possibly something is removing the Kotlin Reflect dependency or is optimising the code away https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.reflect/-k-class/is-value.html
j
found this PR that introduced the error: https://github.com/mockk/mockk/pull/849/files#diff-cc9165ffdb2924dbff17a9920f68cc73e8e4d892abeacd983fce4197a2514e02R62 They even left a comment saying that
KClass.isValue
can throw an exception. The question is why does it happen then
j
So, just to chime in, since I’m working with @Jorge Domínguez… In our project, we use Koin in a slightly convoluted way, as we’re a library, and depend on the lifecycle provided by the host application to load/unload Koin. This is done through an
object
instance, which gives us access to a
koinInstance
, provides methods to initialise and release our Koin instance, and create specific scopes within. We also rely on Koin for a handful of message types the library’s communication is built around (e.g. we use a relative timestamp, which requires a TimestampProvider, which is injected via Koin). To unit/integration test these, we’ve created a custom
TestRule
that, during evaluation, mocks the whole. aforementioned
object
, and forces it to return a custom Koin instance we can test with. This object mocking, and the custom Koin instance return is what causes the above error. A somewhat simplified version of the setup we have is:
Copy code
object Di {
    private var koinInstance: Koin? = null

    fun init(context: Context, vararg modules: Module): Koin { 
        // Koin init here
    }

    fun koin() = koinInstance?.let { it } ?: throw CustomException("We handle de-init of DI here")

    fun release()
}

fun DISupportRule : TestRule {
    // ... MockK'd definitions of providers we need
    val stuffProvider = mockk<StuffProvider>(relaxed = true)

    private val module = module {
        single { stuffProvider } 
    }

    private val testKoinInstance = koinApplication {
        modules(module)
    }

    // This is the method that causes the above crash
    private fun setup() {
        mockkObject(Di)
        every { Di.koin() } returns testKoinInstance
    }

    private fun teardown() {
        unmockkobject(Di)
    }

    override fun apply(...): Statement {
        return object : Statement() {
            override fun evaluate() {
                setup()
                try { base.evaluate() } finally { teardown() }
            }
        }
    }
}
a
that gives a bit more context, but it’s still hard to guess what might be going wrong… I suspect the value class exception is a symptom rather than a cause. MockK has to check every class to determine if it’s a value class, otherwise it can’t figure out what fake object to make. It’s similar to how MockK handles interfaces - it doesn’t mock the interface, it actually tries to find a concrete implementation of the interface and generates a fake object based on that. I guess the cause is some problem to do with mocking a singleton object that seems to have some amount of lazy initialisation (as is indicated by
UninitializedPropertyAccessException
). Or possibly Kotlin Reflect isn’t fully available, so when MockK tries to check for a value class, it fails.
j
The problem is that beyond the fact it happens in the above
init()
method, there’s no further clarity from MockK where it is actually coming. Our init method is also pretty straightforward:
Copy code
if (koinInstance == null) {
    koinInstance = koinApplication {
        androidContext(context.applicationContext)
        modules(modules.toList())
    }.koin
    koinInstance?.createScope(OUR_CUSTOM_SCOPE_ID, OUR_CUSTOM_SCOPE_NAME)
}
return koin()
We’re using Android API 28-33 for running these tests
And please note that as @Jorge Domínguez said, the issue prior to version 1.12.5 of MockK didn’t exist - the new
KClass.isValue()
implementation is the breaking change.
a
what language level of Kotlin are you targeting? 1.5 or above?
what happens if you try and run a test that only contains
Di::class.isValue
?
j
Yes, we are running on Kotlin 1.6.20 at the moment, with some immediate plans to migrate to 1.7.x
329 Views