如何使用 JUnit 和多个服务运行端到端测试

如何使用 JUnit 和多个服务运行端到端测试

问题背景

在尝试使用 JUnit 和 RestTemplates 运行端到端测试时,面对多模块 Spring/Maven 项目,遇到了一些依赖冲突问题。项目结构如下:

txt

parent
|-- service-1
|-- service-2
|-- service-3
|-- integration-test

integration-test 模块的 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 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>
遇到的问题

尝试在 @BeforeAll 方法中启动所有服务时,遇到了类路径冲突的问题。具体来说,Service 1 使用 Spring Cloud Gateway,而 Service 2 使用 Spring MVC。这导致 Service 1 抛出异常,因为它的类路径上存在 Spring MVC:

java

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
原因分析

这个问题的根本原因是,在同一个 JVM 进程中启动多个 Spring Boot 应用程序时,它们会共享相同的类路径,从而导致依赖冲突。Spring Cloud Gateway 检测到类路径上存在 Spring MVC,因此抛出了异常。

解决方案

为了避免在同一个 JVM 进程中启动多个应用程序,可以选择启动独立的进程来运行每个服务。这虽然不是最理想的方案,但能够有效解决类路径冲突问题。

修改 integration-test POM

首先,扩展 integration-test 模块的 POM 文件,确保集成测试类只在 Maven verify 阶段执行:

xml

<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>
在测试类中启动服务

使用以下代码在 E2EIntegrationTest 类中启动和停止各个服务:

java

@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class E2EIntegrationTest {

    private static final String[] services = {"service-1", "service-2", "service-3"};
    private static Process[] instances;

    @BeforeAll
    static void setUp() throws IOException, InterruptedException {
        String baseDir = new File(".").getCanonicalFile().getAbsolutePath();
        log.info("Basedir is: {}", baseDir);

        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++) {
            File jarDir = new File(baseDir + "\\" + services[i] + "\\target\\");
            File[] files = jarDir.listFiles((dir, name) -> name.endsWith(".jar"));
            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 {}", services[i], jarFiles[i].getAbsolutePath());
        }

        for (int i = 0; i < services.length; i++) {
            String command = "java -Dspring.profiles.active=dev -jar %s".formatted(jarFiles[i].getAbsolutePath());
            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);
        }
    }

    @AfterAll
    static void tearDown() {
        for (int i = 0; i < services.length; i++) {
            log.info("Stopping Service {}", services[i]);
            instances[i].destroy();
        }
    }
}

辅助类 ProcessStdOutPrinter 用于输出日志:

java

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(line -> System.out.printf("%s\t: %s%n", serviceName, line));
    }
}

这个解决方案虽然不是最理想的,但可以有效地避免类路径冲突问题。通过在单独的进程中启动各个服务,确保它们之间的依赖不会相互干扰。这种方法特别适用于无法使用 Docker 或其他容器化解决方案的测试环境。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值