Modules Make javac Easy: Part. 2, Dependencies and Tests

by: Ethan McCue

This is a follow-up to this post.

The biggest things I left out in the workflow I was describing are how to handle external dependencies and how to run tests.

On the one hand, I feel like I understand how those would work today with the tools that exist. On the other, I'm pretty sure it can be done a little better.

Try to focus on whether the "shape" of the process feels alright to you and less on the specifics of any particular command.


I wrote a post on this before, but I made a tool called jresolve. It resolves transitive dependencies.1

If you want to get it to follow along you can use this script.

bash < <(curl -s

Or download a .jar from GitHub Releases.

You can use jresolve to download libraries you want to have into a folder.

jresolve --output-directory libs \

This will include any transitive dependencies of those libraries.

jresolve --print-tree \             
org.springframework.boot/spring-boot-starter-web 3.3.0
  . org.springframework.boot/spring-boot-starter 3.3.0
    . org.springframework.boot/spring-boot 3.3.0
      . org.springframework/spring-core 6.1.8

The pkg:maven string is available at the top of the page for any artifact on Maven Central's Search.

If the list of dependencies gets too long you can put the dependencies you want in a file, say libs.txt.


Then include that file with an @ at the end of the command.

jresolve --output-directory libs @libs.txt

Which puts all your dependencies in one place, easily addable to the module path.

javac \
    -d build/javac \
    --module-path libs \
    --module-source-path "./*/src" \
    --module web.hello
java --module-path libs:build/jar --module web.hello

Running Tests

JUnit has a command line launcher. It's not perfect yet and it's not on anything like SdkMan, but it is good enough for our purposes.

Add the dependencies you need for the command line launcher and for writing tests to your libs.txt.2


Make a module for your tests. And make it an open module so the test runner can do its magic.

open module web.hello.test {
    requires web.hello;
    requires org.junit.jupiter.api;

Write a test in this module.

import org.junit.jupiter.api.Test;
import web.hello.HelloController;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class HelloControllerTest {
    public void getHello() {
                new HelloController().index(),
                "Greetings from Spring Boot!"

Then you can launch the test runner like any other code.

java \
    --module-path libs:build/jar \
    --add-modules web.hello.test,web.util.test \
    --module org.junit.platform.console \
    execute \
    --select-module web.hello.test

Which is a little long - I have hopes in the future I can write something like the following.

junit \
    execute \
    --module-path libs:build/jar \
    --select-module web.hello.test

But the basics are that you launch junit, point it at your code, and run tests.

Wrap Up

While all this is more work than adding a dependency to a pom.xml and running mvn test, I'm not convinced its more complicated or any less powerful.

If anything the fact that doing things this way lets us interact more directly with tools like javac makes it feel more flexible.

I made a repo with this setup using Spring Boot that you can find here. All the commands you would run are in the Justfile. I included all the libraries needed in the repo in case you don't want to install my CLI tool for whatever reason.

1: Its gauche to pitch your own tool. Especially that one which is admittedly incomplete. One alternative is Coursier.

2: I know, I know - dependency scopes. This is a relatively large conversation to have, but with the module path things that aren't also "in the graph" aren't included. Having test dependencies in the same `libs` folder as other dependencies isn't as important as with the class path. Yes, making a docker image with just the dependencies needed for runtime needs scopes / a practice emulating it. I'm working my way around.

<- Index