javamavenmaven-shade-pluginmaven-failsafe-pluginmaven-invoker-plugin

How to run maven integration tests on shaded artifact?


I have a simple goal: I want to be able to use the maven-failsafe-plugin, or any viable alternative, to run tests against a jar I'm building with the maven-shade-plugin. Specifically, I want to run the tests after shade runs because I want an integration test that validates shade's relocation didn't break the thing I'm trying to relocate as it often can.

As I'm trying to specifically relocate Jackson, it is important to make sure Jackson is still able to find annotations/etc. on certain POJOs so they (de)serialize correctly. Obviously, that works pre-relocation. We have ran into issues with the shaded jar not (de)serializing things correctly so it is important to us to have a test of some kind that can validate this behavior pre-deploy.

The issue I'm running into appears to be that the maven-failsafe-plugin is running the shaded jar in some capacity but testing the original source. Meaning, it is failing to load a class I relocated even though the relocation process in the maven-shade-plugin should've (and does in the live artifact) relocated the reference to that class.

What I expect: the maven-failsafe-plugin should run entirely off the shaded sources. If not, something else should allow me to run a similar test using the shaded/relocated code at build/CI time. E.g. as though I ran it from the command line. The following, btw, is the output I get from doing so:

>java -jar shade-integration-tests-1.0-SNAPSHOT.jar
{key=123}
test.shaded.com.fasterxml.jackson.databind.ObjectMapper

Exception I get from the test. Note, it is not an assertion failure but a Class loading failure (no other Exceptions occur):

Running test.shade.integration.tests.JsonIT
Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.057 s <<< FAILURE! - in test.shade.integration.tests.JsonIT
test.shade.integration.tests.JsonIT.testClassName  Time elapsed: 0.032 s  <<< ERROR!
java.lang.NoSuchFieldError: MAPPER
    at test.shade.integration.tests.JsonIT.testClassName(JsonIT.java:9)
    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.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:727)
    at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
    at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:156)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:147)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:86)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(InterceptingExecutableInvoker.java:103)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.lambda$invoke$0(InterceptingExecutableInvoker.java:93)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:92)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:86)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:217)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
...

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>test</groupId>
    <artifactId>shade-integration-tests</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.6.1</version>
                <configuration>
                    <source>14</source>
                    <target>14</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.4.0</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <transformers>
                                <transformer
                                    implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <manifestEntries>
                                        <Main-Class>test.shade.integration.tests.MainClass</Main-Class>
                                    </manifestEntries>
                                </transformer>
                            </transformers>
                            <relocations>
                                <relocation>
                                    <pattern>com.fasterxml.jackson</pattern>
                                    <shadedPattern>test.shaded.com.fasterxml.jackson</shadedPattern>
                                </relocation>
                            </relocations>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.0.0-M7</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-failsafe-plugin</artifactId>
                <version>3.0.0-M7</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>integration-test</goal>
                            <goal>verify</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.fasterxml.jackson</groupId>
                <artifactId>jackson-bom</artifactId>
                <version>2.12.6</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.junit</groupId>
                <artifactId>junit-bom</artifactId>
                <version>5.9.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

MainClass.java (can probably ignore this):

package test.shade.integration.tests;

import java.util.Map;

public class MainClass {
    public static void main(String[] args) throws Exception {
        Map<String, Object> result = Json.MAPPER.readValue("{\"key\":123}", Map.class);
        System.out.println(result);
        System.out.println(Json.MAPPER.getClass().getCanonicalName());
    }
}

Json.java

package test.shade.integration.tests;

import com.fasterxml.jackson.databind.ObjectMapper;

public class Json {
    public static final ObjectMapper MAPPER = new ObjectMapper(); // This is part of the problem

    private Json() {
    }
}

JsonTest.java (maven-surefire-plugin, junit test that works fine):

package test.shade.integration.tests;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class JsonTest {
    @Test
    void testClassName() {
        String result = Json.MAPPER.getClass().getCanonicalName();

        assertEquals("com.fasterxml.jackson.databind.ObjectMapper", result);
    }
}

JsonIT.java (maven-failsafe-plugin, IT that fails):

package test.shade.integration.tests;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class JsonIT {
    @Test
    void testClassName() {
        String result = Json.MAPPER.getClass().getCanonicalName();

        assertEquals("test.shaded.com.fasterxml.jackson.databind.ObjectMapper", result);
    }
}

Solution

  • The problem is that the dependencies of your module to the to-be-shaded classes are of course always on the dependency list as well, if you run tests within the same module or even in a dependent module in a multi-module Maven reactor. Therefore, you want to run your ITs in isolation, i.e. not from within the same reactor. The easiest way to do that is Maven Invoker Plugin, which is used in many OSS projects in order to achieve just that. See for instance how it is done in a project I am contributing to, AspectJ Maven Plugin.