kotlin-testrunner icon indicating copy to clipboard operation
kotlin-testrunner copied to clipboard

Does not work on JDK 16+ due to JEP-396 (strong encapsulation)

Open kriegaex opened this issue 4 years ago • 0 comments

de.jodamob.kotlin.testrunner.NoMoreFinalsClassLoader uses Javassist which tries to make protected java.lang.ClassLoader.defineClass accessible. Due to JEP-396, this is forbidden since JDK 16 and cannot be overriden by any --add-opens call either.

Please update the product in order to use another API.


FYI, I use Spock and my workaround for ignoring tests using SpotlinTestRunner on JDK 16+ looks as follows, using a delegation pattern:

package de.scrum_master.testing

import de.jodamob.kotlin.testrunner.SpotlinTestRunner
import org.junit.runner.Description
import org.junit.runner.manipulation.Filter
import org.junit.runner.manipulation.NoTestsRemainException
import org.junit.runner.manipulation.Sorter
import org.junit.runner.notification.RunNotifier
import org.junit.runners.model.InitializationError
import org.spockframework.runtime.Sputnik

class JEP396AwareSpotlinTestRunner extends Sputnik {
  private static final int javaMajor = Integer.parseInt(System.properties.getProperty("java.version").split("[.]")[0])
  public static final boolean hasJEP396 = javaMajor >= 16

  private static final Filter ignoreAllFilter = new Filter() {
    @Override
    boolean shouldRun(Description description) { return false }

    @Override
    String describe() { return "ignores all tests" }
  }

  private final Sputnik sputnik

  JEP396AwareSpotlinTestRunner(Class<?> clazz) throws InitializationError {
    super(clazz)
    sputnik = hasJEP396 ? this : new SpotlinTestRunner(clazz)
  }

  @Override
  void filter(Filter filter) throws NoTestsRemainException {
    if (hasJEP396)
      super.filter(ignoreAllFilter)
    else
      sputnik.filter(filter)
  }

  @Override
  void run(RunNotifier notifier) {
    if (hasJEP396)
      super.run(notifier)
    else
      sputnik.run(notifier)
  }

  @Override
  Description getDescription() {
    if (hasJEP396)
      return super.getDescription()
    else
      return sputnik.getDescription()
  }

  @Override
  void sort(Sorter sorter) {
    if (hasJEP396)
      super.sort(sorter)
    else
      sputnik.sort(sorter)
  }
}

The Spock spec still needs an @IgnoreIf or @Requires in addition to @RunWith(JEP396AwareSpotlinTestRunner), but otherwise no changes are required. It can even utilise a constant JEP396AwareSpotlinTestRunner.hasJEP396 for its own condition:

package de.scrum_master.stackoverflow

import de.jodamob.kotlin.testrunner.OpenedClasses
import de.jodamob.kotlin.testrunner.OpenedPackages
import de.scrum_master.testing.JEP396AwareSpotlinTestRunner
import org.junit.runner.RunWith
import spock.lang.IgnoreIf
import spock.lang.Specification

/**
 * See https://stackoverflow.com/q/48391716/1082681
 * See https://github.com/dpreussler/kotlin-testrunner
 */
@RunWith(JEP396AwareSpotlinTestRunner)
@IgnoreIf({ JEP396AwareSpotlinTestRunner.hasJEP396 })
@OpenedClasses(FinalClass)
//@OpenedPackages("de.scrum_master.stackoverflow")
class AnotherClassSpotlinRunnerTest extends Specification {
  def "use SpotlinRunner to stub final method in final class"() {
    given:
    FinalClass finalClass = Stub() {
      finalMethod() >> "mocked"
    }

    expect:
    new AnotherClass().doSomething(finalClass) == "mocked"
  }
}

The workaround is necessary, because JUnit 4 runners are initialised very early in the lifecycle, even before SpockConfig.groovy or global Spock extensions, even more so before @IgnoreIf and @Requires can kick in.

This is ugly, of course. There are alternatives to the Kotlin test runner, e.g. my own tool Sarek which can do a lot of fancy things, unfinalising classes for JUnit 4/5, TestNG and Spock just being one of them. But before I developed it, I was using your tool in Spock 1.3 and today just noticed that my old example code was no longer when running on JDK 16. So I thought, I take some time to create this issue, FWIW.

kriegaex avatar Aug 14 '21 06:08 kriegaex