mockk: java.lang.InstantiationError when upgrading to Kotlin 1.7

Java 17 + Kotlin 1.7 + Mockk 1.12.4/Spring Mockk 3.1.1

java.lang.InstantiationError: com.example.demo.UpdateAccountResult
    at jdk.internal.reflect.GeneratedSerializationConstructorAccessor18.newInstance(Unknown Source)
    at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:499)
    at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480)
    at org.objenesis.instantiator.sun.SunReflectionFactoryInstantiator.newInstance(SunReflectionFactoryInstantiator.java:48)

It occurred whens using coEvery{} to do some stub, which returns a UpdateAccountResult.Success().

Here UpdateAccountResult is a sealed class, it has some detailed result subclass/sub data classes.

And UpdateAccountResult extends from a base abstract class like this.

abstract class ApiBaseResult {
    @field:JsonProperty("SC")
    val statusCode: String? = null

    @field:JsonProperty("EC")
    val errorCode: String? = null

    @field:JsonProperty("EM")
    val errorMessage: String? = null
}

These codes are working well with Kotlin 1.6.21.

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 60
  • Comments: 57 (13 by maintainers)

Commits related to this issue

Most upvoted comments

Any update for this issue, is there any workaround?

I’m currently having issues publishing to oss.sonatype.org: https://issues.sonatype.org/browse/OSSRH-83030

As soon as this is resolved, I’ll publish a new release.

For those still looking at this issue like me, it was fixed in [this PR](https://github.com/mockk/mockk/pull/939), which was released in 1.13.3.

Thanks for providing more information! I’ve managed to reproduce it in the project tests in my refactoring PR #855.

The sealed class tests fail on 17 and 18, but pass on 11, as reported: https://github.com/mockk/mockk/actions/runs/2827760564 This is a step towards a solution…

For the sake of finishing the PR, I’ll disable the tests, then the problem can be investigated properly later.

Hey, I see from https://github.com/stuebingerb/mockk/pull/1 that all of the tests now pass, is this good to merge and be included in a new MockK release? My projects are waiting to upgrade to Kotlin 1.7 due to this mockk failure. Thanks!

@cliffred that matches what I am seeing locally with 1.13.2.

I wonder if this should be re-opened or a more specific issue raised?

@Raibaz may we have a new release please now that https://github.com/mockk/mockk/pull/916 got merged? Thanks in advance!

Done, v1.13.2 is out 😃

Apparently something changed with sealed classes in 1.7.

However, I couldn’t find anything in the release notes: the only mention of sealed classes is that non-exhaustive sealed hierarchies now trigger an error rather than a warning.

Apparently Kotlin 1.7 in combination with Java 17 uses Java sealed classes.

I compiled the following code with Kotlin 1.6 and 1.7 and then decompiled.

sealed class Foo
class Bar : Foo()

Kotlin 1.7 decompiled:

public abstract sealed class Foo permits Bar {
   private Foo() {
   }

   // $FF: synthetic method
   public Foo(DefaultConstructorMarker $constructor_marker) {
      this();
   }
}

Kotlin 1.6 decompiled:

public abstract class Foo {
   private Foo() {
   }

   // $FF: synthetic method
   public Foo(DefaultConstructorMarker $constructor_marker) {
      this();
   }
}

Tried but the test workflows are not so automated for first-time contributors 😉 https://github.com/mockk/mockk/pull/916

Great ideas, would you be able to give them a go?

  1. fork the MockK repo
  2. make some changes
  3. make a PR against ~the MockK repo~ your own fork master branch, which will automatically trigger the tests for all JVM versions
  4. check the test results
  5. Create a PR against the MockK repo
  6. gain eternal glory in the MockK patch notes 😃

@aSemy Hi.

Is there any update?

https://github.com/mockk/mockk/pull/916 attempts to fix the issue & hasn’t been merged yet.

But the code has not been merged in the main repo, right? The new release do not include the fixing code.

I’ve done research into this and I believe the problem is happening in ObjensesisInstantiator

https://github.com/mockk/mockk/blob/6c571a651e74e66fe364b802a83849bfc7d6e802/modules/mockk-agent/src/jvmMain/kotlin/io/mockk/proxy/jvm/ObjenesisInstantiator.kt#L23-L43

Basically this tries to create a dummy, but valid, instance of a class. MockK can then keep track of this dummy instance, and where it is used. For primitives this is simple, MockK just creates a random primitive. For classes it’s a little more complicated, as MockK has to create an instance that conforms to that class’ requirements. Normally that’s okay, especially if the class is abstract or open. However, a sealed class looks like it’s abstract and open… except for it’s not.

So there are two problems:

  1. MockK can’t tell if a class is sealed. In ObjensesisInstantiator a Java Class<T> is used, not a KClass<T>, and so finding out if the class is sealed is more difficult
  2. How can MockK create a dummy instance of a sealed class? It needs to find the valid subclasses are available. If it can do that, then it can pick some random (non-sealed) subclass, and generate an instance in the usual manner.

I tried refactoring MockK to accept a KClass<T>, and so gain access to the isSealed and sealedSubclasses methods from Kotlin Reflect, but this requires a lot of refactoring, and this broke many other parts of the code.

override fun <T : Any> instance(cls: KClass<T>): T? {
    return when {
        cls.java == Any::class.java -> {
            @Suppress("UNCHECKED_CAST")
            Any() as T
        }

        cls.isSealed -> {
            cls.sealedSubclasses.firstNotNullOfOrNull { subCls ->
                runCatching { instance(subCls) }.getOrNull()
            } ?: error("$cls has no non-sealed subclasses")
        }

        !Modifier.isFinal(cls.java.modifiers) -> {
            try {
                instantiateViaProxy(cls.java)
            } catch (ex: Exception) {
                log.trace(
                    ex,
                    "Failed to instantiate via proxy $cls (${ex::class.simpleName} ${ex.message}), doing objenesis instantiation"
                )
                instanceViaObjenesis(cls.java)
            }

        }

        else -> instanceViaObjenesis(cls.java)
    }
}

This problem isn’t impossible to overcome, but it’s not a quick fix. I’m sharing my work here, so hopefully someone else can take a look and either find another fix, or find something I missed.

Still problematic when upgrading to 1.12.7.

Due to issue #909, I used mockk-jvm in my project, but still encountered exception of java.lang.InstantiationError when using Java 17/Kotlin sealed class.

Thanks for the suggestion, here a reproduction project: https://github.com/ThanksForAllTheFish/mockk832

Probably related: Getting the following error

io.mockk.MockKException: Can't instantiate proxy for class com.acme.FlowConfiguration
...
Caused by: java.lang.IncompatibleClassChangeError: class com.acme.FlowConfiguration$Subclass15 cannot inherit from sealed class com.acme.FlowConfiguration

Where code is:

sealed class FlowConfiguration constructor(val flowName: String)

and mock is:

flowConfigurationMock = mockk {
  every { flowName } returns "testFlow"
}

Worked well in 1.6.10 / Java 17 with Mockk 1.12.2

1.13.2 only partly solves the issue:

// given the following sealed class
sealed class Foo
class FooSub : Foo()

// and the following class with a method that has function with a sealed class as parameter
class Bar {
    fun fooParam(foo: Foo): Int = 42
}

// then the following now works
val foo : Foo = mockk() // Fixed since 1.13.2

// but when using any() where a sealed class is expected the issue still persists
val bar: Bar = mockk()
every { bar.fooParam(any()) } returns 1 // throws InstantiationError

This can probably be fixed in JvmSignatureValueGenerator with the same trick: Passing the first subclass to instantiator.instantiate() in case cls is sealed.

Right, Java 17 added a sealed class feature, and I guess Kotlin is supporting that in the bytecode for Kotlin sealed classes when using Kotlin 1.7 with Java 17.

We encounter the same issue with sealed classes and interfaces with version 1.12.4 when we try to upgrade to Kotlin 1.7.0. Happens in the coEvery every, verify and coVerify blocks throwing a java.lang.InstantiationError.

Same behaviour for simple every {}

Hey sorry about the delay, releasing 1.12.8 now.

Can you tell, why java.lang.Class<T> is not enought in this case?

From my understanding, java.lang.Class also has this data (.isSealed() method and .getPermittedSubclasses()). And starting from Kotlin 1.7, when using 17+ target bytecode level, kotlin generates classes that are also sealed in JVM terms. For older bytecode versions current issue is not applicable, so doesn’t matter if JVM says that class is not sealed

Class.isSealed() is only available on JVM 17+. I tried using it #915, but then MockK won’t compile on JVM 11: https://github.com/mockk/mockk/runs/8178643128?check_suite_focus=true#step:6:72

MockK can’t tell if a class is sealed. In ObjensesisInstantiator a Java Class<T> is used, not a KClass<T>, and so finding out if the class is sealed is more difficult

Can you tell, why java.lang.Class<T> is not enought in this case?

From my understanding, java.lang.Class also has this data (.isSealed() method and .getPermittedSubclasses()). And starting from Kotlin 1.7, when using 17+ target bytecode level, kotlin generates classes that are also sealed in JVM terms. For older bytecode versions current issue is not applicable, so doesn’t matter if JVM says that class is not sealed

@aSemy

I have cloned the project and the reason it works is because it is using Java 8 and not Java 17. https://github.com/mockk/mockk/blob/master/build.gradle https://github.com/mockk/mockk/blob/master/mockk/common/build.gradle.kts

@ygaller it seems related to sealed class.

hey @aSemy I created a project recreating the issue https://github.com/odin-delrio/mockk-issue-832

The key point is that the JVM target for the kotlin compilation options must also be set to reproduce the issue (I’m using a build matrix for the tests):

image (The env var is set in the GH action when running the tests, getting the value from the testing matrix)

Here you have the link to the failing build when using JDK 17 https://github.com/odin-delrio/mockk-issue-832/runs/7561938775?check_suite_focus=true#step:4:428

image

The same build with JDK 11 succeeded.