4

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);
    }
}
kriegaex
  • 63,017
  • 15
  • 111
  • 202
Logic32
  • 43
  • 6

1 Answers1

1

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.

kriegaex
  • 63,017
  • 15
  • 111
  • 202
  • That makes sense and is regrettable. It took literally all day to get the maven-invoker-plugin working correctly and had enough configuration I can't really post the full results here. At least, in the full project I based my dumbed-down question off of. With that said, I hope the maven-failsafe-plugin gets an update in the future that simplifies this workflow somehow. For now, thank you! – Logic32 Nov 03 '22 at 23:30
  • Really? With some copy & paste from the sample project I linked to, it should have been easy to set up. One thing to keep in mind with shaded artifacts is to include dependency-reduced POMs instead of the original ones in order to eliminate the original dependencies. Otherwise those would end up in your Invoker builds again. I would not expect Failsafe to help you in the future with regard to this problem, because the ways I described is simply the way Maven as a whole works: dependencies end up on the class path. – kriegaex Nov 04 '22 at 09:45
  • Mistakes were made. Having never used the maven-invoker-plugin before I spent more time than I care to admit reading the docs. Then I included a parent pom in my test which I probably shouldn't have done. I also spent some time troubleshooting/optimizing some tangential stuff that probably wasn't necessary. But, after things "clicked" with respect to things were supposed to work it wasn't that bad. I also agree that I don't expect maven-failsafe-plugin to do anything. But, it'd still be nice if the process could be simplified a bit. – Logic32 Nov 04 '22 at 15:15
  • @sam-orozco, the OP has accepted my answer, but you put the bounty on the question. Would you mind telling me if this also solves your problem? Otherwise, what else would you like to know? – kriegaex Nov 04 '22 at 15:35