Search code examples
javaspringspring-bootjunitend-to-end

JUnit run End-to-End Test with multiple Services


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.


Solution

  • 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.