10

I'm trying to transform the following library into Java 9 module: https://github.com/sskorol/test-data-supplier

Followed this guide: https://guides.gradle.org/building-java-9-modules

After some manipulations and refactoring (couldn't manage lombok issues, so just temporary removed it), I have the following module-info.java:

module io.github.sskorol {
    exports io.github.sskorol.core;
    exports io.github.sskorol.model;

    requires testng;
    requires vavr;
    requires streamex;
    requires joor;
    requires aspectjrt;
}

And it even compiles / builds in case of tests' skipping. However, when I try to run a test task, I'm getting the following exception:

org.gradle.api.internal.tasks.testing.TestSuiteExecutionException: Could not complete execution for Gradle Test Executor 2.
    at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:63)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:564)
    at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:35)
    at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
    at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:32)
    at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:93)
    at com.sun.proxy.$Proxy1.stop(Unknown Source)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker.stop(TestWorker.java:120)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:564)
    at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:35)
    at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
    at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:146)
    at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:128)
    at org.gradle.internal.remote.internal.hub.MessageHub$Handler.run(MessageHub.java:404)
    at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:63)
    at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:46)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
    at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:55)
    at java.base/java.lang.Thread.run(Thread.java:844)
Caused by: org.testng.TestNGException: 
Cannot instantiate class io.github.sskorol.testcases.DataSupplierTests
    at testng@6.11/org.testng.internal.ObjectFactoryImpl.newInstance(ObjectFactoryImpl.java:31)
    at testng@6.11/org.testng.internal.ClassHelper.createInstance1(ClassHelper.java:410)
    at testng@6.11/org.testng.internal.ClassHelper.createInstance(ClassHelper.java:323)
    at testng@6.11/org.testng.internal.ClassImpl.getDefaultInstance(ClassImpl.java:126)
    at testng@6.11/org.testng.internal.ClassImpl.getInstances(ClassImpl.java:191)
    at testng@6.11/org.testng.TestClass.getInstances(TestClass.java:99)
    at testng@6.11/org.testng.TestClass.initTestClassesAndInstances(TestClass.java:85)
    at testng@6.11/org.testng.TestClass.init(TestClass.java:77)
    at testng@6.11/org.testng.TestClass.<init>(TestClass.java:42)
    at testng@6.11/org.testng.TestRunner.initMethods(TestRunner.java:423)
    at testng@6.11/org.testng.TestRunner.init(TestRunner.java:250)
    at testng@6.11/org.testng.TestRunner.init(TestRunner.java:220)
    at testng@6.11/org.testng.TestRunner.<init>(TestRunner.java:161)
    at testng@6.11/org.testng.SuiteRunner$DefaultTestRunnerFactory.newTestRunner(SuiteRunner.java:578)
    at testng@6.11/org.testng.SuiteRunner.init(SuiteRunner.java:185)
    at testng@6.11/org.testng.SuiteRunner.<init>(SuiteRunner.java:131)
    at testng@6.11/org.testng.TestNG.createSuiteRunner(TestNG.java:1383)
    at testng@6.11/org.testng.TestNG.createSuiteRunners(TestNG.java:1363)
    at testng@6.11/org.testng.TestNG.runSuitesLocally(TestNG.java:1217)
    at testng@6.11/org.testng.TestNG.runSuites(TestNG.java:1144)
    at testng@6.11/org.testng.TestNG.run(TestNG.java:1115)
    at org.gradle.api.internal.tasks.testing.testng.TestNGTestClassProcessor.runTests(TestNGTestClassProcessor.java:129)
    at org.gradle.api.internal.tasks.testing.testng.TestNGTestClassProcessor.stop(TestNGTestClassProcessor.java:88)
    at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:61)
    ... 25 more
Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make public io.github.sskorol.testcases.DataSupplierTests() accessible: module io.github.sskorol does not "exports io.github.sskorol.testcases" to module testng
    at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:337)
    at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:281)
    at java.base/java.lang.reflect.Constructor.checkCanSetAccessible(Constructor.java:192)
    at java.base/java.lang.reflect.Constructor.setAccessible(Constructor.java:185)
    at testng@6.11/org.testng.internal.ObjectFactoryImpl.newInstance(ObjectFactoryImpl.java:22)
    ... 48 more

It seems a bit confusing to me, as io.github.sskorol.testcases is a part of src/test/java and there's no module-info for tests. So I can't export this package to TestNG. Have an assumption that the root cause in a TestNG reflection usage within ObjectFactoryImpl against test classes.

Does anyone have any idea how to workaround it?

Environment: JDK 9 (build 9+181), Gradle 4.1, TestNG 6.11

Serhii Korol
  • 843
  • 7
  • 15

3 Answers3

6

Have an assumption that the root cause in a TestNG reflection usage within ObjectFactoryImpl against test classes.

It's one of two causes, yes. The other is that, apparently, Gradle runs your tests as a module. As you point out, there's no module descriptor for your tests. Gradle may use --patch-module to add the tests to the module containing the production code.

This question and answer provides a lot of background information and possible fixes. As a short term fix, I recommend to add opens io.github.sskorol.testcases to your production code's module descriptor. Judging by its name, I'd guess there is no such package yet, so you'd either have to rename or add a dummy class (I would prefer the former).

I would also take this issue to a Gradle mailing list or bug tracker. Unless we've overlooked something (entirely possible), Gradle's behavior is very unfortunate because it would require adapting the production code's module descriptor to the test code's needs.

Nicolai Parlog
  • 47,972
  • 24
  • 125
  • 255
  • Thanks, will check these options. – Serhii Korol Aug 14 '17 at 12:34
  • Nicolai, I've tried to add `opens`, and it did the trick. However, all the tests have failed. The most popular exception was related to reflection usage in my main code. E.g. **joor** couldn't access classes within **src/test/java**, and asked to export corresponding package, similar to the initial issue with TestNG. So I guess there's no easy workaround on this, and would be better to raise an issue in Gradle tracker. – Serhii Korol Aug 14 '17 at 13:29
  • @Nikolai, what else should Gradle be doing than use `--patch-module`? It seems the only way to give the tests access to the production code. – Stefan Oehme Aug 18 '17 at 16:15
  • 1
    Some people argue, unit tests should not run as a module and instead use the class path. (I'm not so sure about that, but it has some advantages.) – Nicolai Parlog Aug 18 '17 at 17:01
2

If the tests are in the same package as the module under test then they need to be compiled (with --patch-module) so that they are compiled "as if" they are part of the module. The references to TestNG types in the tests means that that --add-reads io.github.sskorol=testng will be needed too.

Running is similar. The tests need to be run "as if" they are in module io.github.sskorol. This means running with:

--patch-module io.github.sskorol=<testclasses> \
--add-reads io.github.sskorol=testng

Additionally, you may need to export or open the packages with the tests to TestNG. This will come down to whether the test classes and methods are public in an exported package. To avoid scanning, the simplest is to open the all packages containing tests to TestNG, e.g.

--add-opens io.github.sskorol/io.github.sskorol.core=testng \
--add-opens io.github.sskorol/io.github.sskorol.core.internal=testng

(.internal is just a filler for a non-exported package in the module)

All this might look complicated but it's something that the Maven Surefire plugin and also Gradle should do.

Nicolai Parlog
  • 47,972
  • 24
  • 125
  • 255
Alan Bateman
  • 5,283
  • 1
  • 20
  • 25
  • I'm surprised by `--add-reads io.github.sskorol=testng`. TestNg uses reflection to look into _io.github.sskorol_ - shouldn't that establish readability? – Nicolai Parlog Aug 31 '17 at 09:23
  • The tests will use the TestNG API (org.testng.Assert.assertTrue for example) so the module does need to read testng. – Alan Bateman Aug 31 '17 at 19:49
  • Argh, I read the clause the wrong way around. Yes, that makes sense. More than that, every test dependency (mocking, assertion, ... library) has to be added that way. – Nicolai Parlog Sep 01 '17 at 06:33
  • @AlanBateman thanks, I've just tried these tricks, and it seems to be working now, except 1 part, related to SPI. As in Java 9 we don't require `META-INF/services` anymore, and should use `provides ... with ...` in `module-info.java` instead, I'm wondering if it's possible to specify this information via `jvmargs`? As tests don't have `module-info`, but I still have to provide implementation class, which is located within 1 of test packages. Is there any flag for that? – Serhii Korol Sep 17 '17 at 15:39
0

Did some tricks suggested by @AlanBateman, who gave me a valid direction.

Finally, I came up with the following configuration:

module-info.java

module io.github.sskorol {
    exports io.github.sskorol.core;
    exports io.github.sskorol.model;

    opens io.github.sskorol.utils to joor;

    requires testng;
    requires vavr;
    requires streamex;
    requires joor;
}

build.gradle

test {
    inputs.property("moduleName", moduleName)
    doFirst {
        jvmArgs = [
                '--module-path', classpath.asPath,
                '--add-modules', 'ALL-MODULE-PATH',
                '--add-opens', 'io.github.sskorol/io.github.sskorol.testcases=testng',
                '--add-opens', 'io.github.sskorol/io.github.sskorol.testcases=joor',
                '--add-opens', 'io.github.sskorol/io.github.sskorol.datasuppliers=joor',
                '--add-opens', 'io.github.sskorol/io.github.sskorol.datasuppliers=testng',
                '--add-opens', 'java.base/java.util=streamex',
                '--add-opens', 'java.base/java.util.stream=streamex',
                '--patch-module', "$moduleName=" + files(sourceSets.test.java.outputDir).asPath
        ]
        classpath = files()
    }
}

Both testng and joor required access to my test packages. So --add-opens flag did the trick. streamex module also had concerns accessing java.base packages.

Note that in comparison with original Java 8 code I had to remove lombok and aspectj dependencies, as I couldn't manage to fully resolve all the issues occurred after migration.

Another problem I've faced with was related to SPI testing. According to docs I've read, in Java 9 SPI implementations should be listed within module-info.java instead of META-INF/services. But it doesn't seem to be a solution, when implementation class is located in one of test packages, as again test is not a module. Just wondering if there's some jvmflag for replacing provides ... with ... syntax, and doing the same trick as with --add-opens. Any thoughts would be appreciated.

Full implementation, modified to support Java 9, could be found here.

Serhii Korol
  • 843
  • 7
  • 15