I am trying to run End-to-End tests with JUnit and RestTemplates.
I have a Spring / Maven Multi Module Project in the following structure:
parent
|-- service-1
|-- service-2
|-- service-3
|-- integration-test
The integration-test POM looks like that:
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>my.project</groupId>
<artifactId>parent</artifactId>
<version>${revision}</version>
</parent>
<artifactId>integration-test</artifactId>
<dependencies>
<dependency>
<groupId>my.project</groupId>
<artifactId>service-1</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>my.project</groupId>
<artifactId>service-2</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>my.project</groupId>
<artifactId>service-3</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
I tried the following code to start all services: (See here: https://stackoverflow.com/a/47873906/23364885)
...
@BeforeAll
static void setUp() throws Exception {
Properties s1p = getProperties("../service-1/src/test/resources/application.yml");
SpringApplication s1 = new SpringApplicationBuilder(Service1Application.class)
.properties(s1p).build();
s1.setAdditionalProfiles("dev");
s1.run();
Properties s2p = getProperties("../service-2/test/main/resources/application.yml");
SpringApplication s2 = new SpringApplicationBuilder(Service2.class)
.properties(s2p).build();
s2.setAdditionalProfiles("dev");
s2.run();
Properties s3p = getProperties("../service-3/test/main/resources/application.yml");
SpringApplication s3 = new SpringApplicationBuilder(Service3.class)
.properties(s3p).build();
s3.setAdditionalProfiles("dev");
s3.run();
}
My Problem is, the applications are using the full / mixed context / dependencies from all services.
For example, Service 1 is using Spring Cloud Gateway.
Service 2 is using Spring MVC.
Now, Service 1 is throwing an Error because it has Spring MVC on its classpath.
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.cloud.gateway.config.GatewayClassPathWarningAutoConfiguration$SpringMvcFoundOnClasspathConfiguration': Failed to instantiate [org.springframework.cloud.gateway.config.GatewayClassPathWarningAutoConfiguration$SpringMvcFoundOnClasspathConfiguration]: Constructor threw exception
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:1317)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1202)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:562)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:326)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:324)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:975)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:962)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:624)
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754)
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:456)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:334)
at my.package.integration.FullFlowIT.setUp(FullFlow.java:33)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.cloud.gateway.config.GatewayClassPathWarningAutoConfiguration$SpringMvcFoundOnClasspathConfiguration]: Constructor threw exception
at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:221)
at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:88)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:1311)
17 more
Caused by: org.springframework.cloud.gateway.support.MvcFoundOnClasspathException
at org.springframework.cloud.gateway.config.GatewayClassPathWarningAutoConfiguration$SpringMvcFoundOnClasspathConfiguration.<init>(GatewayClassPathWarningAutoConfiguration.java:45)
at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:499)
at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480)
at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:195)
19 more
I can't use docker in my test environment. I also don't want to start jar files (see here: https://stackoverflow.com/a/56694218/23364885) if I can anyhow avoid it.
When i start all Service "by hand" with mvn spring-boot:run in each modules directory, all of them start with no problems.
Unfortunately, the only solution I could find was one that I originally wanted to avoid. I got the basic idea from here: https://stackoverflow.com/a/56694218/23364885
First, I extended the POM from the Integration Test module:
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<excludes>
<exclude>**/E2EIntegrationTest</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>integrationtest</id>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>test</goal>
</goals>
<phase>integration-test</phase>
<configuration>
<excludes>
<exclude>none</exclude>
</excludes>
<includes>
<include>**/E2EIntegrationTest</include>
</includes>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
For example, the class with the integration tests is only executed on the Maven Goal verify with the integrationtest profile. For example:
mvn clean verify -Pintegrationtest
With Goal Verify, the individual jars are already built, which is why I start / stop them in class E2EIntegrationTest as follows:
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class E2EIntegrationTest {
private static final String[] services = {"authorization-server", "devproxy", "portal", "gateway"};
private static Process[] instances;
@BeforeAll
static void setUp() throws IOException, InterruptedException {
// Find all Executables
String baseDir = new File(".").getCanonicalFile().getAbsolutePath();
log.info("Basedir is: {}", baseDir);
// Fix if started from Parent Folder
if (baseDir.contains("integration-test"))
baseDir = new File(".").getCanonicalFile().getParentFile().getAbsolutePath();
log.info("Basedir is: {}", baseDir);
File[] jarFiles = new File[services.length];
instances = new Process[services.length];
for (int i = 0; i < services.length; i++) {
FileFilter fileFilter = new WildcardFileFilter("*.jar"); // ToDo Fix
File jarDir = new File(baseDir + "\\" + services[i] + "\\target\\");
File[] files = jarDir.listFiles(fileFilter);
assert files != null;
jarFiles[i] = Arrays.stream(files)
.filter(f -> !f.getAbsolutePath().contains("javadoc"))
.filter(f -> !f.getAbsolutePath().contains("sources"))
.findFirst()
.orElseThrow();
log.info(
"Found for Service {} Jar File {}, executable {}",
services[i],
jarFiles[i].getAbsolutePath(),
jarFiles[i].canExecute());
}
log.info("Checkpoint: All Jar-Files found!");
// Start all Services
for (int i = 0; i < services.length; i++) {
String command =
"java -Dspring.profiles.active=dev -jar %s --spring.datasource.url=jdbc:h2:file:./target/e2e-test;AUTO_SERVER=true;Mode=Oracle --spring.jpa.hibernate.ddl-auto=create-drop"
.formatted(jarFiles[i]);
log.info("Starting Service {} with command {}", services[i], command);
instances[i] = Runtime.getRuntime().exec(command);
Executors.newSingleThreadExecutor().submit(new ProcessStdOutPrinter(services[i], instances[i]));
Thread.sleep(5000);
}
log.info("Checkpoint: All Services started!");
}
@AfterAll
static void tearDown() {
for (int i = 0; i < services.length; i++) {
log.info("Stopping Service {}", services[i]);
instances[i].destroy();
}
}
// Test Methods ...
}
To see the logs of the individual services, I used the following helper class:
public class ProcessStdOutPrinter implements Runnable {
private final InputStream inputStream;
private final String serviceName;
public ProcessStdOutPrinter(String serviceName, Process process) {
this.serviceName = serviceName;
this.inputStream = process.getInputStream();
}
@Override
public void run() {
new BufferedReader(new InputStreamReader(inputStream))
.lines()
.forEach(l -> System.out.printf("%s\t: %s%n", serviceName, l));
}
}
It's not an optimal solution, but it works.