Unit testing with Java 9, Jigsaw and JUnit

Java 9 is on it's way and with it - maybe - Project Jigsaw. Project Jigsaw adds modularization to Java, which will change the way we write Java code. The recent public review ballot for JSR #376 (the Java Platform Module System, or JPMS for short) however produced a historic result in that a majority of the participants voted not to support the JSR in its current state[1]. With this post I hope to explain how project modularization would be used with the current state of JSR #376 and thereby where some of the critique comes from.

The example I'll be using is how one may write unit tests with Jigsaw. There are be some rather unexpected changes to the way we write those when using Jigsaw modularization; there are two reasons for this:

  1. With modules, the file structure of a Java project changes.
  2. Unit tests (usually) require external libraries, which with Jigsaw have to become modules.

Some basic examples for using Jigsaw can be found in the Project Jigsaw: Module System Quick-Start Guide and some more complete examples in this Github repo. But those don't cover everything in detail, so let's look at an example of writing a unit test with JUnit and Project Jigsaw.


Disclaimer

All of these examples are based on an early access version of JDK 9 with Project Jigsaw, which can be downloaded here.
In my examples I'm using build 165; since Jigsaw is still in development, things probably won't work exactly like this in future versions.

To check your java version, use the following command:

$ java -version
java version "9-ea"
Java(TM) SE Runtime Environment (build 9-ea+165)
Java HotSpot(TM) 64-Bit Server VM (build 9-ea+165, mixed mode)

The new project file structure and module-info.java

The differences start when creating a Jigsaw project. My base file structure looks like this:

$ tree src/
src/
└── com.senacor.greeting
    ├── main
    │   └── java
    │       └── com
    │           └── senacor
    │               └── greeting
    │                   ├── Hello.java
    │                   └── Main.java
    ├── module-info.java
    └── test
        └── java
            └── com
                └── senacor
                    └── greeting
                        └── HelloTest.java

11 directories, 4 files

We can see some key differences to the common project structure used by Maven projects here:

  1. Our source code lies within a directory called com.senacor.greeting.
  2. There is a module-info.java file[2].

By convention, everything belonging to a module should be within a directory with the same name as that module. We're creating a module called com.senacor.greeting, so that's what our directory is called.

The module-info.java file in its very simplest form looks like this:

module com.senacor.greeting {}

This module neither requires any other modules (except for the base module which is included automatically) nor does it provide anything for other modules to use. It includes the restricted keyword[3] module, the modules name and then empty braces. That's enough for the beginning. We'll be adding entries to the module-info.java file later.

Adding some code

Our non-test classes look like this:

package com.senacor.greeting;

public class Main {
    public static void main(String[] args) {
        System.out.println(new Hello().greet(args));
    }
}
package com.senacor.greeting;

import java.util.Arrays;
import java.util.stream.Collectors;

public class Hello {
    public String greet(String... names) {
        if ((names == null) || (names.length == 0)) {
            return "Hello World";
        } else {
            return Arrays.stream(names)
                    .map(name -> String.format("Hello %s!", name))
                    .collect(Collectors.joining("\n"));
        }
    }
}

Not too exciting; just Java 8 here so far. In fact, if you ignore the slightly weird file structure and the module-info.java file, this project looks exactly like a Java 8 project would.

Compiling and running a Jigsaw project

In its most simple form, a Jigsaw project can be compiled very similarly to a project using a prior Java version:

$ javac -d "target/com.senacor.greeting" \
> src/com.senacor.greeting/module-info.java \
> src/com.senacor.greeting/main/java/com/senacor/greeting/Hello.java \
> src/com.senacor.greeting/test/java/com/senacor/greeting/HelloTest.java \
> src/com.senacor.greeting/main/java/com/senacor/greeting/Main.java

Or, if you'd prefer a shorter version using the unix command line tool find:

$ javac -d "src/com.senacor.greeting" $(find src -name "*.java")

Really, the only difference to prior versions of Java at this point is, that we're also compiling the module-info.java file. So far, so easy. The result in both cases will look like this:

$ tree target/
target/
└── com.senacor.greeting
    ├── com
    │   └── senacor
    │       └── greeting
    │           ├── Hello.class
    │           ├── HelloTest.class
    │           └── Main.class
    └── module-info.class

4 directories, 4 files

Of course we can run our project with the command line as well:

$ java --module-path=target \
> --module com.senacor.greeting/com.senacor.greeting.Main
Hello World

or

$ java --module-path=target \
> --module com.senacor.greeting/com.senacor.greeting.Main \
> Alice Bob Charlie
Hello Alice!
Hello Bob!
Hello Charlie!

We have to add the module path (using --module-path=<path>) and tell java which module our main class is in (using --module <module name>/<class name>)[4]. Interesting, but covered in the quick starts I mentioned earlier.

Adding libraries

Jigsaw has not yet made it into any final JDK version, so of course the use within third party libraries is extremely limited. This was considered when designing Jigsaw and so there are mechanisms for creating so called automatic modules from such libraries.
In our example we'll be adding a third party library for direct use and one which is a transitive dependency[5]. The direct dependency is JUnit 4.12 which can be downloaded from Maven Central as a jar file. I downloaded it into a directory called lib which sits next to src and target. So the project now looks like this:

$ tree src target lib
src
└── com.senacor.greeting
    ├── main
    │   └── java
    │       └── com
    │           └── senacor
    │               └── greeting
    │                   ├── Hello.java
    │                   └── Main.java
    ├── module-info.java
    └── test
        └── java
            └── com
                └── senacor
                    └── greeting
                        └── HelloTest.java
target
└── com.senacor.greeting
    ├── com
    │   └── senacor
    │       └── greeting
    │           ├── Hello.class
    │           ├── HelloTest.class
    │           └── Main.class
    └── module-info.class
lib
└── junit-4.12.jar

15 directories, 9 files

JUnit 4.12 has not been specifically modified for Jigsaw, which we can see by calling the following command:

$ jar -d --file=lib/junit-4.12.jar
No module descriptor found. Derived automatic module.

module junit@4.12 (automatic)
  requires mandated java.base
  contains junit.extensions
  contains junit.framework
  contains junit.runner
  contains junit.textui
  contains org.junit
##################### shortened for readability #####################

Here we see, that an automatic module with the name junit is created and it's version 4.12. Sounds great! Let's add that to our module-info.java:

module com.senacor.greeting {
    requires junit;
}

Note that we do not list the library version here; Jigsaw is not meant to replace tools like Maven or Gradle and does not care about the specific versions[6].
We'll now add a simple test and try to compile everything:

package com.senacor.greeting;

import org.junit.Test;
import static org.junit.Assert.assertEquals;

public class HelloTest {
    @Test
    public void passingSeveralArguments_returnsMultipleGreeting() {
        Hello hello = new Hello();

        assertEquals("Hello Alice!\nHello Bob!", hello.greet("Alice", "Bob"));
    }
}
$ javac --module-path lib \
      -d "target/com.senacor.greeting" $(find src -name "*.java")

Note the --module-path argument here; it tells the Java compiler, that it can find modules in the given directories. The compilation succeeded - so far, so good. Let's try running our code:

$ java --module-path=lib:target \
> --add-modules com.senacor.greeting \
> --module junit/org.junit.runner.JUnitCore \
> com.senacor.greeting.HelloTest

JUnit version 4.12
Exception in thread "main" java.lang.NoClassDefFoundError: org/hamcrest/SelfDescribing
	at java.base/java.lang.ClassLoader.defineClass1(Native Method)
##################### shortened for readability #####################
	at junit@4.12/org.junit.runner.JUnitCore.main(JUnitCore.java:36)
Caused by: java.lang.ClassNotFoundException: org.hamcrest.SelfDescribing
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:552)
##################### shortened for readability #####################

Oh dear. It seems we're missing something. Especially these two lines are relevant:

    at junit@4.12/org.junit.runner.JUnitCore.main(JUnitCore.java:36)
Caused by: java.lang.ClassNotFoundException: org.hamcrest.SelfDescribing 

It seems a dependency is missing: Hamcrest. This is the transitive dependency I mentioned earlier. There are ways of knowing of such dependencies beforehand however: A tool called jdeps[7] has been part of the JDK since version 8 and with JDK 9 it can give us some interesting information:

$ jdeps -s lib/junit-4.12.jar
junit-4.12.jar -> java.base
junit-4.12.jar -> java.management
junit-4.12.jar -> not found

The not found part tells us, that something is amiss. The -s parameter means, that we only want a summary; instead this time we'll grep the complete output for the string "not found".

$ jdeps lib/junit-4.12.jar | grep "not found"
junit-4.12.jar -> not found
   org.junit                      -> org.hamcrest                   not found
   org.junit.experimental.results -> org.hamcrest                   not found
   org.junit.internal             -> org.hamcrest                   not found
   org.junit.internal.matchers    -> org.hamcrest                   not found
   org.junit.matchers             -> org.hamcrest                   not found
   org.junit.matchers             -> org.hamcrest.core              not found
   org.junit.rules                -> org.hamcrest                   not found

So, Hamcrest is a dependency. But more than that: Hamcrest is the only missing, non-transitive dependency of JUnit - only the modules org.hamcrest and org.hamcrest.core show up which are both provided by the hamcrest-core library.[8]
No problem, we can get that from Maven Central too. Sadly we're not told this, but we want version 1.3 of the hamcrest-core jar. Adding that to the lib directory and then recompiling and rerunning our code produces very different results:

$ java --module-path=lib:target \
> --add-modules com.senacor.greeting \
> --module junit/org.junit.runner.JUnitCore \
> com.senacor.greeting.HelloTest

JUnit version 4.12
.E
Time: 0.012
There was 1 failure:
1) passingSeveralArguments_returnsMultipleGreeting(com.senacor.greeting.HelloTest)
java.lang.IllegalAccessException: class org.junit.runners.BlockJUnit4ClassRunner (in module junit) cannot access class com.senacor.greeting.HelloTest (in module com.senacor.greeting) because module com.senacor.greeting does not export com.senacor.greeting to module junit
	at java.base/jdk.internal.reflect.Reflection.newIllegalAccessException(Reflection.java:370)
##################### shortened for readability #####################
	at junit@4.12/org.junit.runner.JUnitCore.main(JUnitCore.java:36)

FAILURES!!!
Tests run: 1,  Failures: 1

Luckily, reading the errors here gives us a very good hint towards what's wrong:

class org.junit.runners.BlockJUnit4ClassRunner (in module junit) cannot access class com.senacor.greeting.HelloTest (in module com.senacor.greeting) because module com.senacor.greeting does not export com.senacor.greeting to module junit

OK, so com.senacor.greeting is not exported to the junit module although that's required. Should be easy to fix:

module com.senacor.greeting {
    requires junit;
    exports com.senacor.greeting to junit;
}
$ java --module-path=lib:target \
> --add-modules com.senacor.greeting \
> --module junit/org.junit.runner.JUnitCore \
> com.senacor.greeting.HelloTest

JUnit version 4.12
...
Time: 0.029

OK (3 tests)

And there we have it: Unit tests running with JDK 9 and Project Jigsaw. Hurray!

Conclusions

Using Jigsaw modules requires a few changes to the way we interact with third party libraries; especially if those libraries have to access our classes. If you know what you're doing, those changes are not that difficult; however the changes required for projects atempting to use Jigsaw modularization can quite easily lead to a split in the Java world between those using Jigsaw and those not using it.
This is just one of a number of critiques towards JSR #376 in its current state.[9]

After the ballot result[1:1], Oracle has a 30 day window to make adjustments to the current implementation before the next ballot. General availability of JDK 9 and Project Jigsaw is currently still scheduled for July 27th 2017[10]. Whether that date can be met will have to be seen.


Footnotes

Images

The original image of Duke was licensed by Sun Microsystems Inc. in 2007 with the following notice:

Copyright © Sun Microsystems Inc.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
  1. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  1. Neither the name of Sun Microsystems Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY SUN MICROSYSTEMS INC. "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SUN MICROSYSTEMS INC. BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


  1. The results of the May 8th 2017 public review ballot can be found here. ↩︎ ↩︎

  2. The module-info.java file is explained in JPMS: Modules in the Java Language and JVM, chapter 1.1. ↩︎

  3. Restricted keywords are only keywords within a module declaration. A full list can be found in this draft of the The Java Language Specification Java SE 9 Edition, chapter 3.9. ↩︎

  4. Module paths were suggested in JEP 261 and are explained in The State of the Module System, chapter 2.1. ↩︎

  5. The transitive dependency in this case is hamcrest-core, as we could have learned from the JUnit "Download and Install" page if I weren't trying to show a more general method of identifying dependencies. ↩︎

  6. Quote: "[The Jigsaw specification] need not define yet another dependency-management mechanism. Maven, Ivy, and Gradle have all tackled this difficult problem. We should leave it to these and other build tools, and container applications, to discover and select a set of candidate modules for a given library or application. The module system need only validate that the set of selected modules satisfies each module’s dependences." (from http://openjdk.java.net/projects/jigsaw/spec/reqs/02#version-selection) ↩︎

  7. jdeps documentation for JDK 8 ↩︎

  8. A library can expose more than one module and several libraries can expose the same module so there's no simple way to identify which libraries exactly are required with Jigsaw alone. This is further proof that Jigsaw cannot (and doesn't intend to) fully replace package managers. ↩︎

  9. Some of the arguments against JSR #376 in its current state can be found in this blogpost by RedHats Scott Stark, in this mailing list entry by IBMs Tim Ellison and in this (German language) interview with Hazelcasts Christoph Engelbert. ↩︎

  10. The up-to-date release schedule of JDK 9 can be viewed on the JDK 9 project page. ↩︎