Search code examples
javaspringspring-bootgradlemulti-module

Unable to integration-testing gradle multi-module Spring-Boot-Application


Within a Gradle multi-module project with the bootstrapping in its own module I'm unable to use MockMvc, because its need to reference the bootstrapping-module. I'm not sure if I have misconfigured something. The basic structure is:

  • module: a module containing some REST-Services and needs a testImplementation-Dependency on starter
  • starter: the bootstrapping-module which gets the spring-boot-plugin applied and depends on module

I have set up a minimal example on github using Spring-Boot 2.3.1.RELEASE and Gradle 6.4 with the following configuration:

./settings.gradle.kts

rootProject.name = "spring-multimodule-integrationtest"
include("starter", "module")

./build.gradle.kts

subprojects {
    repositories {
        jcenter()
    }
    dependencies {
        apply(plugin = "java-library")
        "testImplementation"("junit:junit:4.12")
    }
}

./starter/build.gradle.kts

plugins {
    id("org.springframework.boot") version "2.3.1.RELEASE"
}

dependencies {
    implementation(project(":module"))
}

./module/build.gradle.kts

dependencies {
    testImplementation(project(":starter"))
}

The starter-module contains only one a single class "Starter" referencing the module-module:

public class Starter {
    public String info() { return "starter"; }
    public static void main(String[] args) {
        System.out.println(new Starter().info() + " and " + new Module().info());
    }
}

The module-module (*sigh I should have chosen a different name for this module) contains only this implemenation-class:

public class Module {
    public String info() { return "module"; }
}

Additionally, the module-module has the following test-class doing the integration-test:

public class IntegrationTest
{
    @Test public void testSomeLibraryMethod() {
        final ByteArrayOutputStream out = new ByteArrayOutputStream();
        System.setOut(new PrintStream(out));
        Starter.main(new String[0]);
        assertEquals("starter and module\n", out.toString());
    }
}

This code runs fine until the applying of the spring-boot-plugin within "./starter/build.gradle.kts". When the tasks "clean test" issued on the shell I get:

❯ ./gradlew clean test

> Task :module:test FAILED

de.kramhal.multi.IntegrationTest > testSomeLibraryMethod FAILED
    java.lang.NoClassDefFoundError at IntegrationTest.java:17
        Caused by: java.lang.ClassNotFoundException at IntegrationTest.java:17

1 test completed, 1 failed

This problem does not occur, when tests are executed within the IDE (IntelliJ to be exact).

I already tried unsuccessfully to use the spring-dependency-management as suggested in this answer (as well as in several other answers).

What have I done wrong?


Solution

  • First off, I would recommend restructuring your project so you don't have cyclic dependencies. As it is now, in order to build starter, you need to build module. And in order to test module, you need to build starter. Gradle can do it, but it is usually a smell.

    In terms of troubleshooting: when you get a test failure like this, look at the test report as that has the full stack trace. You should see that it complains that it can't find the Starter class (Caused by: java.lang.ClassNotFoundException: de.kramhal.multi.Starter), which is of cause in the starter module.

    You mentioned the spring-dependency-management plugin, but that is only relevant for managing Maven dependencies, and not project dependencies like this. So it is not helpful here.

    I am not entirely sure if this is Windows specific or not as I remember there were some discussions around performance a while back when having a lot of classes. But I believe the java-library plugin will look for jar files in other projects, and not the folder for compiled classes. This is a problem for you since the spring-boot plugin will by default disable the standard jar task and instead create "fat" a jar file through the bootJar task. Because you need both the fat jar for packaging the application to run stand-alone but also the normal jar for consuming it as a dependency, you need to do some tweaks to the starter project (Kotlin DSL):

    tasks {
        jar {
            enabled = true
        }
        bootJar {
            archiveClassifier.set("boot")
        }
    }
    

    This will enable the normal jar file, but because the name will conflict with the one produced by the bootJar task, you need to rename one of them. I chose to rename the bootJar one.

    I don't know why the test works for you in IntelliJ as that should, by default, delegate everything to Gradle. But maybe you have an old version, or done some manual configuration to let IntelliJ both compile and run your tests.