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需要激活testprofile 才会按该 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 环境探测异常。
备注:内容仅供参考。
