Spring Office Hours Podcast: S5E19 - Docker, Compose, Testcontainers, Oh My!

2026-07-27 30 预计阅读时间: 1 分钟
来源: spring.io AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:14 分钟

{"title_zh":"从 Docker Compose 到 Testcontainers:让 Spring 开发环境与集成测试使用同一套基础设施","body_zh":"# 从 Docker Compose 到 Testcontainers:让 Spring 开发环境与集成测试使用同一套基础设施\n\nDocker、Compose 与 Testcontainers 经常同时出现在 Spring 项目里,却解决着不同层次的问题:Docker 负责封装服务,Compose 负责组织本地依赖,Testcontainers 则把真实基础设施带进自动化测试。把三者放在一条工作流里,关键不是‘容器化一切’,而是让开发、测试和 CI 对数据库、消息队列等依赖形成一致且可重复的约定。\n\n## 三种工具,各自负责哪一段\n\nDocker 提供镜像和容器这两个基本构件。对 Spring 应用而言,它最常用于运行 PostgreSQL、Redis、Kafka 等外部服务,也可以用于打包应用本身。\n\nDocker Compose 在 Docker 之上描述一组需要协同运行的服务。开发者执行一条命令,就能获得固定版本、固定端口和固定初始化参数的本地环境。它适合人工开发和联调,因为服务可以持续运行,数据也可以通过卷保留。\n\nTestcontainers 面向测试生命周期。测试启动时创建容器,动态取得连接信息,测试结束后清理资源。它特别适合验证 JDBC、事务、数据库方言、迁移脚本和消息协议等无法由内存替身可靠覆盖的行为。\n\n一个实用的职责划分是:\n\n- 本地日常开发使用 Compose。\n- 集成测试使用 Testcontainers。\n- CI 直接运行同一组 Testcontainers 测试。\n- 应用镜像由 Docker 构建,但不要让测试依赖一个长期存在的共享环境。\n\n## 用 Compose 建立可重复的本地数据库\n\n下面是一个可以直接改造的 PostgreSQL 示例。将它保存为 compose.yaml,然后执行 docker compose up -d。运行前可按项目需要修改数据库名、用户名和密码。\n\nyaml\nservices:\n postgres:\n image: postgres:16-alpine\n environment:\n POSTGRES_DB: orders\n POSTGRES_USER: app\n POSTGRES_PASSWORD: app-secret\n ports:\n - \"5432:5432\"\n healthcheck:\n test: [\"CMD-SHELL\", \"pg_isready -U app -d orders\"]\n interval: 5s\n timeout: 3s\n retries: 10\n volumes:\n - postgres-data:/var/lib/postgresql/data\n\nvolumes:\n postgres-data:\n\n\n启动并检查服务:\n\nbash\ndocker compose up -d\ndocker compose ps\ndocker compose logs -f postgres\n\n\n对应的 Spring Boot 本地配置可以这样写:\n\nyaml\nspring:\n datasource:\n url: jdbc:postgresql://localhost:5432/orders\n username: app\n password: app-secret\n jpa:\n hibernate:\n ddl-auto: validate\n\n\n这里使用 validate,是为了尽早暴露实体模型与数据库结构不一致的问题。实际项目可以配合 Flyway 或 Liquibase 管理迁移,而不是依赖 Hibernate 在不同环境中自动建表。\n\n## 用 Testcontainers 验证真实 PostgreSQL 行为\n\n本地 Compose 环境不应该成为测试前置条件。测试可以自行启动 PostgreSQL,并把随机端口和凭据注入 Spring。下面假设项目使用 Spring Boot 3.1 或更高版本、JUnit 5、Maven,以及 Spring Boot 的 Testcontainers 集成。\n\n在 pom.xml 中加入测试依赖:\n\nxml\n<dependencies>\n <dependency>\n <groupId>org.springframework.boot</groupId>\n <artifactId>spring-boot-starter-test</artifactId>\n <scope>test</scope>\n </dependency>\n <dependency>\n <groupId>org.springframework.boot</groupId>\n <artifactId>spring-boot-testcontainers</artifactId>\n <scope>test</scope>\n </dependency>\n <dependency>\n <groupId>org.testcontainers</groupId>\n <artifactId>postgresql</artifactId>\n <scope>test</scope>\n </dependency>\n</dependencies>\n\n\n创建一个最小集成测试:\n\njava\npackage com.example.orders;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport javax.sql.DataSource;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.boot.testcontainers.service.connection.ServiceConnection;\nimport org.testcontainers.containers.PostgreSQLContainer;\nimport org.testcontainers.junit.jupiter.Container;\nimport org.testcontainers.junit.jupiter.Testcontainers;\n\n@Testcontainers\n@SpringBootTest\nclass DatabaseIntegrationTest {\n\n @Container\n @ServiceConnection\n static PostgreSQLContainer<?> postgres =\n new PostgreSQLContainer<>(\"postgres:16-alpine\");\n\n @Autowired\n DataSource dataSource;\n\n @Test\n void connectsToPostgres() throws Exception {\n try (var connection = dataSource.getConnection();\n var statement = connection.createStatement();\n var result = statement.executeQuery(\"select version()\")) {\n assertThat(result.next()).isTrue();\n assertThat(result.getString(1)).contains(\"PostgreSQL\");\n }\n }\n}\n\n\n@ServiceConnection 让 Spring Boot 从容器读取 JDBC URL、用户名和密码,因此测试不需要硬编码端口。运行时只需要 Docker 兼容的容器环境:\n\nbash\n./mvnw test\n\n\n如果项目版本不支持 @ServiceConnection,可以改用 @DynamicPropertySource 手动注册 spring.datasource.url 等属性。这属于框架版本差异,不应通过固定映射宿主机端口来规避。\n\n## 一致性不等于共用同一个容器\n\nCompose 和 Testcontainers 最值得共享的是服务版本与初始化约定,而不是容器实例。可以在两边统一使用 postgres:16-alpine,并让同一套 Flyway 迁移随应用启动;但集成测试仍应拥有隔离的数据库生命周期。这样可以避免测试顺序、残留数据和开发者本机状态影响结果。\n\n也要注意几个边界:\n\n- 容器测试比纯单元测试慢,应把业务分支留给快速单元测试,只对真实协议和基础设施行为使用 Testcontainers。\n- 镜像标签需要固定到明确版本,避免 CI 因上游 latest 变化而突然失败。\n- 首次运行需要拉取镜像,CI 应合理使用镜像缓存,但不能假设公共镜像仓库永远可用。\n- ARM 与 x86 开发机可能表现不同,选镜像时要确认多架构支持。\n- 测试中的数据库真实,并不意味着整个生产环境已被等价复刻;网络、权限、存储和托管服务限制仍需单独验证。\n\n## 落地时的最小清单\n\n先把一个关键依赖纳入这条工作流,而不是一次迁移全部服务。为本地开发提供 compose.yaml,为关键持久化路径增加 Testcontainers 集成测试,在两处固定相同镜像版本,并在 CI 中执行测试。确认迁移脚本、健康检查和清理机制都稳定后,再扩展到 Redis、Kafka 或其他依赖。\n\n这套组合的价值不在于减少所有环境差异,而在于把最常见、最昂贵的差异变成代码:开发者能看见它,测试能启动它,CI 也能重复验证它。","title_en":"From Docker Compose to Testcontainers: One Infrastructure Contract for Spring Development and Testing","body_en":"# From Docker Compose to Testcontainers: One Infrastructure Contract for Spring Development and Testing\n\nDocker, Compose, and Testcontainers often appear together in Spring projects, but they operate at different levels. Docker packages services, Compose coordinates local dependencies, and Testcontainers gives automated tests disposable instances of real infrastructure. The useful goal is not to containerize everything. It is to make databases, brokers, and other dependencies repeatable across developer machines and CI.\n\n## Give Each Tool a Clear Job\n\nDocker supplies the image and container primitives. A Spring application may use it to run PostgreSQL, Redis, or Kafka, and it may also ship as a container image itself.\n\nDocker Compose describes a group of services for interactive development. One command can create an environment with known image versions, ports, credentials, and initialization settings. Containers can remain available between application restarts, while volumes preserve useful development data.\n\nTestcontainers follows the test lifecycle instead. It creates a service before a test, exposes its runtime connection details, and removes it afterward. That makes it a strong fit for behavior that mocks or in-memory substitutes cannot represent faithfully: SQL dialect details, JDBC behavior, migrations, transactions, and wire protocols.\n\nA practical division of responsibility looks like this:\n\n- Use Compose for everyday local development and manual integration work.\n- Use Testcontainers for automated integration tests.\n- Run those same tests in CI.\n- Build the application with Docker, but do not make tests depend on a long-lived shared environment.\n\n## Create a Repeatable Local Database with Compose\n\nThe following PostgreSQL setup is ready to adapt. Save it as compose.yaml, adjust the database name or credentials if needed, and run docker compose up -d.\n\nyaml\nservices:\n postgres:\n image: postgres:16-alpine\n environment:\n POSTGRES_DB: orders\n POSTGRES_USER: app\n POSTGRES_PASSWORD: app-secret\n ports:\n - \"5432:5432\"\n healthcheck:\n test: [\"CMD-SHELL\", \"pg_isready -U app -d orders\"]\n interval: 5s\n timeout: 3s\n retries: 10\n volumes:\n - postgres-data:/var/lib/postgresql/data\n\nvolumes:\n postgres-data:\n\n\nStart the database and inspect its state with: \nbash\ndocker compose up -d\ndocker compose ps\ndocker compose logs -f postgres\n\n\nA matching Spring Boot development configuration could be: \nyaml\nspring:\n datasource:\n url: jdbc:postgresql://localhost:5432/orders\n username: app\n password: app-secret\n jpa:\n hibernate:\n ddl-auto: validate\n\n\nUsing validate helps expose differences between the entity model and the database schema. In a production-oriented project, Flyway or Liquibase should usually own schema changes rather than relying on Hibernate to create different schemas in different environments.\n\n## Test Against Real PostgreSQL with Testcontainers\n\nThe Compose environment should not be a prerequisite for running tests. A test can start its own PostgreSQL instance and pass its random port and credentials to Spring. The example below assumes Spring Boot 3.1 or newer, JUnit 5, Maven, and Spring Boot's Testcontainers integration.\n\nAdd these test dependencies to pom.xml: \nxml\n<dependencies>\n <dependency>\n <groupId>org.springframework.boot</groupId>\n <artifactId>spring-boot-starter-test</artifactId>\n <scope>test</scope>\n </dependency>\n <dependency>\n <groupId>org.springframework.boot</groupId>\n <artifactId>spring-boot-testcontainers</artifactId>\n <scope>test</scope>\n </dependency>\n <dependency>\n <groupId>org.testcontainers</groupId>\n <artifactId>postgresql</artifactId>\n <scope>test</scope>\n </dependency>\n</dependencies>\n\n\nThen add a minimal integration test: \njava\npackage com.example.orders;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport javax.sql.DataSource;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.boot.testcontainers.service.connection.ServiceConnection;\nimport org.testcontainers.containers.PostgreSQLContainer;\nimport org.testcontainers.junit.jupiter.Container;\nimport org.testcontainers.junit.jupiter.Testcontainers;\n\n@Testcontainers\n@SpringBootTest\nclass DatabaseIntegrationTest {\n\n @Container\n @ServiceConnection\n static PostgreSQLContainer<?> postgres =\n new PostgreSQLContainer<>(\"postgres:16-alpine\");\n\n @Autowired\n DataSource dataSource;\n\n @Test\n void connectsToPostgres() throws Exception {\n try (var connection = dataSource.getConnection();\n var statement = connection.createStatement();\n var result = statement.executeQuery(\"select version()\")) {\n assertThat(result.next()).isTrue();\n assertThat(result.getString(1)).contains(\"PostgreSQL\");\n }\n }\n}\n\n\n@ServiceConnection allows Spring Boot to derive the JDBC URL, username, and password from the running container, so the test does not hard-code a host port. With a Docker-compatible container runtime available, run: \nbash\n./mvnw test\n\n\nProjects on older Spring Boot versions can register spring.datasource.url and related properties with @DynamicPropertySource. That is a framework-version concern; mapping every test database to a fixed host port is not a reliable workaround.\n\n## Consistency Does Not Mean Sharing One Container\n\nCompose and Testcontainers should share service versions and initialization conventions, not container instances. Both can use postgres:16-alpine, and both can apply the same Flyway migrations when the application starts. The test database should still have an isolated lifecycle so test order, stale data, and a developer's local state cannot influence the result.\n\nThere are important limits to keep in view:\n\n- Container tests are slower than unit tests. Keep most business-rule coverage in fast unit tests and reserve containers for infrastructure behavior.\n- Pin explicit image versions instead of relying on latest.\n- The first run must pull images. CI can cache layers, but builds should account for registry availability.\n- Check multi-architecture support when developers and CI use a mix of ARM and x86 machines.\n- A real database container does not reproduce every production property. Network policy, storage, permissions, and managed-service restrictions still require separate validation.\n\n## A Small Adoption Checklist\n\nStart with one dependency that matters, rather than moving every service at once. Add a compose.yaml for local work, cover a critical persistence path with Testcontainers, pin the same image version in both places, and execute the test in CI. Once migrations, health checks, and cleanup are dependable, extend the approach to Redis, Kafka, or other services.\n\nThe benefit of this toolchain is not the elimination of every environment difference. It turns the most common and expensive differences into code that developers can inspect, tests can start, and CI can verify repeatedly.","seo_description_en":"Learn how Docker Compose and Testcontainers create repeatable Spring development and integration-test environments with real PostgreSQL."}


相关推荐