Spring Boot集成测试:Testcontainers启动Auth服务容器失败求助
问题描述
在Spring Boot应用中用Testcontainers编写集成测试,测试用户服务(创建用户时会调用认证服务加密密码),启动两个容器:
- postgres:17数据库容器:启动正常,可执行init.sql
- 自定义构建的auth-service:latest容器:启动失败,报错如下:
Wait strategy failed. Container exited with code 1 Caused by: java.net.ConnectException: Connection refusedWaiting for URL: http://localhost:56873/auth/health ... Timed out waiting for URL to be accessible (http://localhost:56873/auth/health should return HTTP [200])
测试代码:
@SpringBootTest( properties = { "logging.level.org.springframework.security=DEBUG", }, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @Testcontainers @TestPropertySource(properties = { "spring.jpa.hibernate.ddl-auto=create-drop", }) class UserControllerIntegrationTests { static Network network = Network.newNetwork(); @Container static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:17") .withDatabaseName("testdb") .withUsername("testuser") .withPassword("testpass") .withNetwork(network) .withNetworkAliases("postgres-db"); @Container static GenericContainer<?> authService = new GenericContainer<>("auth-service:latest") .withExposedPorts(8080) .withNetwork(network) .withNetworkAliases("auth-service") .waitingFor(Wait.forHttp("/auth/health") .forStatusCode(200) .withStartupTimeout(Duration.ofSeconds(20))); static { String initScriptPath = "./init.sql"; try { MountableFile.forClasspathResource(initScriptPath); System.out.println("✅ Testcontainers: Found init script on classpath: " + initScriptPath); postgres.withInitScript(initScriptPath); } catch (IllegalArgumentException e) { System.err.println("❌ Testcontainers ERROR: Init script NOT found on classpath: " + initScriptPath); throw new RuntimeException("Failed to locate database initialization script.", e); } } @Autowired private TestRestTemplate restTemplate; @Value("${server.api.key}") private String apiKey; @Autowired private UserRepository userRepository; @Autowired private ObjectMapper objectMapper; @Autowired private TestHelper httpTestHelper; @Autowired private JwtService jwtService; @Autowired private JwtAuthFilter jwtAuthFilter; @DynamicPropertySource static void configureProperties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", postgres::getJdbcUrl); registry.add("spring.datasource.username", postgres::getUsername); registry.add("spring.datasource.password", postgres::getPassword); registry.add("api.services.auth-service", () -> "http://" + authService.getHost() + ":" + authService.getMappedPort(8080)); } }
排查与解决方案
1. 查看auth-service容器启动日志
容器退出码1说明服务自身启动失败,先定位具体原因:
- 在测试代码中添加日志输出:
authService.withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger("auth-service"))); - 或者手动启动容器查看日志:
docker run -p 8080:8080 auth-service:latest
常见原因:服务依赖的数据库配置错误、环境变量缺失、端口冲突、代码内部异常等。
2. 修正健康检查的访问方式
如果auth-service的健康接口仅绑定容器内的localhost,宿主机无法直接访问。可以修改等待策略,改为在容器内部检测,或者调整服务的监听地址:
- 方案一:使用日志匹配等待服务启动
.waitingFor(Wait.forLogMessage(".*Started AuthServiceApplication.*", 1) .withStartupTimeout(Duration.ofSeconds(60))) - 方案二:让Testcontainers在容器内部发起健康检查请求
.waitingFor(Wait.forHttp("/auth/health") .forStatusCode(200) .withStartupTimeout(Duration.ofSeconds(60)) .insideContainer()) // 添加这行,使用容器内部网络访问
3. 确保auth-service与postgres的网络连通
如果auth-service需要连接postgres,必须使用容器网络别名postgres-db而非localhost:
- 给auth-service传递正确的数据库环境变量:
static GenericContainer<?> authService = new GenericContainer<>("auth-service:latest") .withExposedPorts(8080) .withNetwork(network) .withNetworkAliases("auth-service") .withEnv("SPRING_DATASOURCE_URL", "jdbc:postgresql://postgres-db:5432/testdb") .withEnv("SPRING_DATASOURCE_USERNAME", "testuser") .withEnv("SPRING_DATASOURCE_PASSWORD", "testpass") .waitingFor(...);
4. 指定容器启动依赖
默认Testcontainers并行启动容器,若auth-service依赖postgres,需明确依赖关系确保postgres先启动完成:
@Container static GenericContainer<?> authService = new GenericContainer<>("auth-service:latest") .dependsOn(postgres) // 添加依赖 .withExposedPorts(8080) .withNetwork(network) .withNetworkAliases("auth-service") .waitingFor(...);
5. 延长启动超时时间
如果auth-service启动较慢,20秒超时不够,延长至60秒或更久:
.waitingFor(Wait.forHttp("/auth/health") .forStatusCode(200) .withStartupTimeout(Duration.ofSeconds(60)))
内容的提问来源于stack exchange,提问作者Razvan Anghel
相关产品推荐
相关产品推荐

