javaspring-bootmavenjunitspock

NoSuchMethodError: 'java.util.Set org.junit.platform.engine.TestDescriptor.getAncestors() when running Spock Spring Boot unit tests


Context

I have a unit test written using the Spock Testing framework to test my Spring Boot application. I've declared the following test dependencies:

Symptom

When I run mvn test, the surefire plugin fails to execute them, giving the error:

[ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:3.2.2:test (default-test) on project myspockspringtest: 
[ERROR] 
[ERROR] Please refer to C:\data\ myspockspringtest\target\surefire-reports for the individual test results.
[ERROR] Please refer to dump files (if any exist) [date].dump, [date]-jvmRun[N].dump and [date].dumpstream.
There was an error in the forked process
'java.util.Set org.junit.platform.engine.TestDescriptor.getAncestors()'

What have I done?

I followed surefire's instructions and looked at the surefire dump file it created, and see the error:

java.lang.NoSuchMethodError: 'java.util.Set org.junit.platform.engine.TestDescriptor.getAncestors()'
    at org.junit.platform.launcher.core.StackTracePruningEngineExecutionListener.getTestClassNames(StackTracePruningEngineExecutionListener.java:50)

I tried declaring Spock's bill of materials, spock-bom to ensure Spock's dependencies are consistent, but this didn't help:

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.spockframework</groupId>
                <artifactId>spock-bom</artifactId>
                <version>2.3-groovy-4.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

I've also tested without the spock-bom declared, but still get this error.

How can I fix this and why is it happening?


Solution

  • How do we fix this?

    The class in the exception, TestDescriptor, comes from the org.junit.platform.engine package.

    Let's run mvn dependency:tree (or the Maven Helper plugin, if using IntelliJ) to see if are multiple versions of org.junit.platform group components for the two dependencies we declared:

    [INFO] --- dependency:3.6.1:tree (default-cli) @ myspockspringtest ---
    ...
    [INFO] |  +- org.junit.jupiter:junit-jupiter:jar:5.10.2:test
    [INFO] |  |  +- org.junit.jupiter:junit-jupiter-api:jar:5.10.2:test
    [INFO] |  |  |  +- org.opentest4j:opentest4j:jar:1.3.0:test
    [INFO] |  |  |  +- org.junit.platform:junit-platform-commons:jar:1.10.2:test
    [INFO] |  |  |  \- org.apiguardian:apiguardian-api:jar:1.1.2:test
    [INFO] |  |  +- org.junit.jupiter:junit-jupiter-params:jar:5.10.2:test
    [INFO] |  |  \- org.junit.jupiter:junit-jupiter-engine:jar:5.10.2:test
    ...
    [INFO] +- org.spockframework:spock-spring:jar:2.3-groovy-4.0:test
    [INFO] |  +- org.spockframework:spock-core:jar:2.3-groovy-4.0:test
    [INFO] |  |  \- org.junit.platform:junit-platform-engine:jar:1.9.0:test
    

    From this we can see that spock-spring the junit-platform-engine: org.junit.platform:junit-platform-engine:jar:1.9.0:test

    Looking at spring-boot-starter-test it contains a similar-looking 1.x version 1.10.2: org.junit.platform:junit-platform-commons:jar:1.10.2:test So maybe these two are conflicting.

    We can also use Ctrl+N in IntelliJ to look up TestDescriptor to see which artifact it comes from. This tells us that TestDescriptor lives in the junit-platform-engine-1.9.0 jar.

    How does Maven resolve which dependency to use?

    Where there is a conflict, Maven maps out the dependencies in a tree and chooses the dependency with the least number of hops to reach it. If there's still a conflict, Maven will choose the one declared first in the pom.

    In this case, the junit-platform-engine from spock-spring is nearer in the hierarchy to the root (3 hops for spock-spring vs 4 hops for spring-boot-starter-test's), so Maven picked Spock's 1.9.0 dependency for junit-platform-engine and

    So we have the following mismatch causing the 1.9.0 engine to find a method that is no longer in 1.10.2:

    So, how do we fix it?

    We have three options:

    Update Spock to 2.4-M1 or later (recommended)

    This issue was resolved in Spock 2.4-M1 so our best option is to use a version of Spock that's 2.4-M1 or later (thanks @leonard). At the time of writing, the latest 2.4.x version was:

    So, we update our spock-spring dependency to:

            <dependency>
                <groupId>org.spockframework</groupId>
                <artifactId>spock-spring</artifactId>
                <version>2.4-M4-groovy-4.0</version>
                <scope>test</scope>
            </dependency>
    

    Our other two options will work for @ContextConfiguration tests but not for @SpringBootTest tests. We need Spock 2.4 (above) for the correct resolution.

    Or

    Use a Maven exclusions element

    We can exclude the junit-platform-engine dependency from spock-spring so that only the one in spring-boot-starter-test is available:

            <dependency>
                <groupId>org.spockframework</groupId>
                <artifactId>spock-spring</artifactId>
                <scope>test</scope>
                <exclusions>
                    <exclusion>
                        <groupId>org.junit.platform</groupId>
                        <artifactId>junit-platform-engine</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
    

    Or

    Include the Spring Boot bill of materials

    We can add the spring-boot-dependencies bom to the dependencyManagement in our pom:

        <dependencyManagement>
            <dependencies>
                <dependency>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-dependencies</artifactId>
                    <version>3.2.4</version>
                    <type>pom</type>
                    <scope>import</scope>
                </dependency>
            </dependencies>
        </dependencyManagement>
    

    This works both with and without the Spock BOM.

    Once we've done this, our mvn dependency:tree for these components now looks like:

    [INFO] --- dependency:3.6.1:tree (default-cli) @ myspockspringtest ---
    ...
    [INFO] |  +- org.junit.jupiter:junit-jupiter:jar:5.10.2:test
    [INFO] |  |  +- org.junit.jupiter:junit-jupiter-api:jar:5.10.2:test
    [INFO] |  |  |  +- org.opentest4j:opentest4j:jar:1.3.0:test
    [INFO] |  |  |  +- org.junit.platform:junit-platform-commons:jar:1.10.2:test
    [INFO] |  |  |  \- org.apiguardian:apiguardian-api:jar:1.1.2:test
    [INFO] |  |  +- org.junit.jupiter:junit-jupiter-params:jar:5.10.2:test
    [INFO] |  |  \- org.junit.jupiter:junit-jupiter-engine:jar:5.10.2:test
    [INFO] |  |     \- org.junit.platform:junit-platform-engine:jar:1.10.2:test
    ...
    [INFO] +- org.spockframework:spock-spring:jar:2.3-groovy-4.0:test
    [INFO] |  +- org.spockframework:spock-core:jar:2.3-groovy-4.0:test
    [INFO] |  \- org.apache.groovy:groovy:jar:4.0.4:test
    ...
    

    From this, we can conclude that the TestDescriptor.getAncestors() method in junit-platform's v1.9 was removed in v1.10.

    To resolve it, we ensure that junit's dependencies are all in sync either by excluding them from our dependencies or by importing a bill-of-materials that declares them all. Or better still, using Spock 2.4 which resolve the issue completely.