PAYATHON 2026

Spring Boot 3 + Testcontainers 集成测试为何被忽略或跳过?

排障赵师傅

日志显示,Testcontainers 在初始化阶段无法连接可用的 Docker daemon,容器没有启动,测试方法也没有执行。应先解决 Docker 连接问题,再检查 Spring 数据源和测试断言。

依赖中还混用了 Testcontainers 1.20.5 和 2.0.1,配置中同时使用了 @ServiceConnection 和 jdbc:tc:。这些配置也需要整理,但现有日志还无法确定 Docker 探测失败的具体原因。

为什么显示 ignored/skipped?

容器启动和 Spring 测试上下文初始化都发生在测试方法执行之前。如果这个阶段失败,IDE 可能将类中的测试显示为未执行或 skipped,具体状态取决于测试运行器和报告方式。

默认的 @Testcontainers 不会在 Docker 不可用时自动跳过测试,只有显式设置以下参数才会启用这一行为:

@Testcontainers(disabledWithoutDocker = true)

题目中的注解没有这个参数,因此需要查看测试类级别的异常和完整控制台输出,不能只凭 IDE 的状态标签判断原因。

日志中的两条信息含义不同:

Could not find a valid Docker environment

表示本次 Docker 环境探测失败。

Previous attempts to find a Docker environment failed. Will not retry.

表示同一 JVM 中此前的探测已经失败,后续调用会直接返回失败。需要向前查找第一次探测时各个策略的具体错误,修复环境后重新启动测试进程。

这段异常链中的 Can't get Docker image 由 Docker 连接失败引起,不能据此认定 MySQL 镜像不存在或镜像仓库不可访问。

为什么 docker-compose up 能成功?

docker-compose up 成功,只能证明运行该命令的进程可以访问 Docker。

Testcontainers 通过 Java Docker 客户端访问 Docker API。测试 JVM 与终端可能使用不同的环境变量、用户权限或 Docker endpoint,例如:

  • 终端使用的 Docker context 指向某个 socket,测试 JVM 没有获得等价配置。
  • IDE 从桌面启动,没有继承 shell 中设置的 DOCKER_HOST。
  • 使用 Colima、rootless Docker 等环境时,socket 不在默认位置。
  • 测试运行在容器或 CI 中,没有访问宿主 Docker daemon 的通道。
  • Java Docker 客户端与 Docker Engine 的 API 不兼容。

排查时需要确认测试 JVM 连接的 endpoint,以及连接失败的原因。

第一步:核对 Docker endpoint 和测试进程环境

先在能够成功执行 Compose 的终端运行:

docker version
docker info
docker context show
docker context inspect --format '{{.Endpoints.docker.Host}}' "$(docker context show)"

docker version 应能显示 Server 信息。然后,在 Linux/macOS 终端检查连接相关的环境变量:

printenv DOCKER_HOST DOCKER_CONTEXT DOCKER_TLS_VERIFY DOCKER_CERT_PATH

将这些信息与 IDE 的运行配置对照。Testcontainers 不一定会按 Docker CLI 相同的方式处理当前 context。

如果当前 context 使用本机 Unix socket,确认路径正确后,可以将该 endpoint 显式传给测试进程。例如,在同一终端运行:

export DOCKER_HOST="$(docker context inspect --format '{{.Endpoints.docker.Host}}' "$(docker context show)")"
./mvnw -Dtest=ArtistControllerIT test

这个示例适用于本机 Unix socket。若 endpoint 使用 SSH 或 TLS,还需要确认 Java 客户端是否支持,以及证书配置是否正确,不能只复制地址。

如果终端测试成功、IDE 测试失败,应检查 IDE 的环境变量、JDK、运行用户和启动方式。修改后重新运行测试,必要时重启 IDE。

如果是 socket 权限问题,应确认测试用户有权访问目标 socket。不要用 chmod 666 或开启无认证 TCP 端口来绕过权限限制。

第二步:获取第一次 Docker 探测的详细日志

可以临时在 src/test/resources/logback-test.xml 中加入:

<configuration>
    <appender name="CONSOLE"
              class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} %-5level %logger{60} - %msg%n</pattern>
        </encoder>
    </appender>

    <logger name="org.testcontainers" level="DEBUG"/>
    <logger name="com.github.dockerjava" level="DEBUG"/>

    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>

如果项目已有测试日志配置,合并 logger 设置即可。

查看最终异常之前的日志,根据具体错误排查:

  • No such file or directory:检查 socket 路径。
  • Permission denied:检查运行用户和 socket 权限。
  • Connection refused:检查 daemon 是否启动、endpoint 是否正确。
  • TLS 或证书错误:检查证书路径及相关变量。
  • Docker API 版本错误:检查 Docker Engine 与实际解析出的客户端版本。

当前堆栈还不足以确定哪一种情况是根因,需要以第一次探测的详细错误为准。

第三步:统一 Testcontainers 版本

当前配置混用了这两个模块:

org.testcontainers:junit-jupiter:1.20.5
org.testcontainers:testcontainers-mysql:2.0.1

它们属于 Testcontainers 的两个主版本,可能导致 Maven 解析出的核心库、扩展和客户端依赖不一致,需要先统一版本。

运行以下命令检查依赖:

./mvnw dependency:tree "-Dincludes=org.testcontainers:*,com.github.docker-java:*"
./mvnw help:effective-pom

题目没有提供 Spring Boot 的具体版本,无法直接给出一套适用于所有 Spring Boot 3 项目的版本组合。

如果项目使用 Spring Boot 的依赖管理,且管理的是 Testcontainers 1.x,可以删除原来显式声明的两个 Testcontainers 依赖,改用以下配置:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-testcontainers</artifactId>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>mysql</artifactId>
    <scope>test</scope>
</dependency>

省略版本号的前提是依赖管理已经生效。Testcontainers 1.x 的 MySQL 类为:

org.testcontainers.containers.MySQLContainer

题目使用的类为:

org.testcontainers.mysql.MySQLContainer

它属于 Testcontainers 2.x 的包结构。如果选择 2.x,需要同时核对模块名称、所有 Testcontainers 依赖的版本,以及与 Spring Boot 集成的兼容性,不能只升级 MySQL 模块。

<java.version>25</java.version> 也需要与具体的 Spring Boot、构建插件和测试工具版本一起核对。现有异常无法证明 Java 25 是根因。

第四步:保留一种数据库容器配置方式

使用 @ServiceConnection 时,Spring Boot 会从容器生成数据库连接信息,通常不需要再配置:

spring:
  datasource:
    url: jdbc:tc:mysql:8.4:///testdb
    driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver

jdbc:tc: 则由 Testcontainers JDBC 驱动根据连接 URL 创建容器,与 @Container 配合 @ServiceConnection 属于不同的配置方式。

当前测试建议保留 @Container 和 @ServiceConnection,删除上述 URL 和驱动配置。服务连接信息会优先于相应的连接属性,因此同时保留两种配置不一定会启动两个容器,但会让实际连接来源更难判断。

还需要检查以下几点:

  • @ServiceConnection 从 Spring Boot 3.1 开始提供。
  • application-test.yml 需要激活 test profile 才会按该 profile 加载,例如使用 @ActiveProfiles("test")。
  • YAML 必须正确缩进。
  • 代码使用 mysql:8.0,日志中却出现 mysql:8.4,说明运行时配置与展示的代码存在差异。需要核对实际执行的测试和容器定义,不能仅凭镜像名确定来源。

一个用于确认数据库连接的最小示例

以下示例使用前述 Testcontainers 1.x 依赖方案,并假设已删除 jdbc:tc: 和 ContainerDatabaseDriver 配置:

import java.sql.Connection;
import java.sql.SQLException;

import javax.sql.DataSource;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.test.context.ActiveProfiles;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import static org.junit.jupiter.api.Assertions.assertTrue;

@Testcontainers
@ActiveProfiles("test")
@SpringBootTest(
    webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
)
class ArtistControllerIT {

    @Container
    @ServiceConnection
    static final MySQLContainer<?> mysql =
        new MySQLContainer<>("mysql:8.0");

    @Autowired
    DataSource dataSource;

    @Test
    void databaseIsAvailable() throws SQLException {
        try (Connection connection = dataSource.getConnection()) {
            assertTrue(connection.isValid(5));
        }
    }
}

这个测试用于确认容器能否启动、Spring 上下文能否初始化,以及数据源能否连接。它仍依赖应用自身的配置。如果迁移脚本或其他 Bean 初始化失败,需要根据新出现的异常继续排查。

要单独排除 Spring 的影响,可以先运行:

import org.junit.jupiter.api.Test;
import org.testcontainers.DockerClientFactory;

class DockerEnvironmentTest {

    @Test
    void dockerApiIsReachable() {
        DockerClientFactory.instance()
            .client()
            .infoCmd()
            .exec();
    }
}
./mvnw -Dtest=DockerEnvironmentTest test

如果这个测试也失败,应先处理 Docker endpoint、权限或客户端兼容性问题。它只检查 Docker API 是否可访问,不能保证后续镜像拉取和容器启动都成功。

测试启动后,还要检查什么?

ArtistControllerIT 通常不匹配 Maven Surefire 的默认测试类命名规则,排查时可以显式运行:

./mvnw -Dtest=ArtistControllerIT test

如果项目通过 Maven Failsafe 管理 *IT,应在配置好插件后使用:

./mvnw verify

“没有发现测试”和“发现测试后初始化失败”需要分别排查。题目中已经出现容器启动堆栈,说明至少这次运行进入了初始化阶段。

原有业务测试中还有两个独立问题:

  • 不应固定断言生成的 ID 为 1L。静态容器会在同一测试类中共享,已有记录和自增序列都会影响结果。
  • exchange("/artists", ..., saved.getId()) 中的最后一个参数用于填充 URI 模板。路径没有 {id} 时,它不会自动成为路径参数或查询参数。应按 Controller 的实际映射构造 URL,并断言 HTTP 状态和响应内容。

这两个问题会影响容器成功启动后的测试结果,但不会导致当前的 Docker 环境探测异常。

备注:内容仅供参考。