Issue
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.
Answered By - kriegaex
Answer Checked By - David Marino (JavaFixing Volunteer)