1. 简介

本文将深入探讨 Java 中的 TestContainers 库。这个工具能让你在单元测试中直接启动 Docker 容器,从而编写真正自包含的集成测试。以往我们常为数据库、消息队列等外部依赖发愁,现在只需一个注解,就能拉起真实环境,彻底告别“本地能跑,CI 报错”的尴尬。

✅ 优势很明显:

  • 测试环境与生产尽可能一致
  • 无需预装数据库或中间件
  • 多人协作时环境高度统一
  • 支持 CI/CD 流水线一键执行

只要某个服务有 Docker 镜像,你就能在测试里用它。比如常见的 PostgreSQL、Kafka、Redis,甚至 Selenium 浏览器自动化场景,TestContainers 都能轻松搞定。


2. 环境要求

TestContainers 支持 Java 8 及以上版本,并兼容 JUnit 4 的 @Rule / @ClassRule 机制(也支持 JUnit 5 扩展模型)。

Maven 依赖

核心库必须引入:

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers</artifactId>
    <version>1.19.6</version>
    <scope>test</scope>
</dependency>

根据使用场景,还需添加专用模块。本文示例会用到 PostgreSQL 和 Selenium:

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>postgresql</artifactId>
    <version>1.19.6</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>selenium</artifactId>
    <version>1.19.6</version>
    <scope>test</scope>
</dependency>

📌 最新版可查看 Maven Central

⚠️ 前提条件:本地或 CI 环境必须安装并运行 Docker 服务。可通过以下命令验证:

docker info

如果连 docker run hello-world 都跑不起来,TestContainers 也无能为力。


3. 基本用法

我们先用 GenericContainer 启动一个极简的 Web 服务容器,演示核心流程。

@ClassRule
public static GenericContainer<?> simpleWebServer = 
    new GenericContainer<>("alpine:3.2")
        .withExposedPorts(80)
        .withCommand("/bin/sh", "-c", 
            "while true; do echo \"HTTP/1.1 200 OK\\n\\nHello World!\" | nc -l -p 80; done");

关键点解析:

  • @ClassRule:容器在测试类执行前启动,所有测试方法共用,结束后销毁
  • withExposedPorts(80):暴露容器内的 80 端口
  • withCommand(...):定义容器启动后执行的命令(这里用 netcat 实现一个 HTTP 响应)

测试中通过 IP 和映射端口访问服务:

@Test
public void givenSimpleWebServerContainer_whenGetRequest_thenReturnsResponse() throws Exception {
    String address = "http://" 
        + simpleWebServer.getContainerIpAddress() 
        + ":" + simpleWebServer.getMappedPort(80);
    
    String response = simpleGetRequest(address); // 假设这是个发 GET 请求的方法
    
    assertEquals("Hello World!", response);
}

✅ 小贴士:

  • 使用 @Rule 则每个测试方法都会启停一次容器(更隔离但更慢)
  • getMappedPort() 返回的是宿主机上动态映射的端口,避免端口冲突

4. 常见使用模式

TestContainers 提供了多种专用容器类型,比 GenericContainer 更省心。下面介绍三种高频场景。

4.1. 数据库集成测试

写 DAO 层或 Repository 测试时,最怕数据库版本不一致或数据污染。TestContainers 的 PostgreSQLContainer 直接帮你搞定。

@Rule
public PostgreSQLContainer<?> postgresContainer = new PostgreSQLContainer<>("postgres:13");

@Test
public void whenSelectQueryExecuted_thenResultsReturned() throws Exception {
    String jdbcUrl = postgresContainer.getJdbcUrl();
    String username = postgresContainer.getUsername();
    String password = postgresContainer.getPassword();

    try (Connection conn = DriverManager.getConnection(jdbcUrl, username, password);
         Statement stmt = conn.createStatement()) {
        
        ResultSet rs = stmt.executeQuery("SELECT 1");
        rs.next();
        int result = rs.getInt(1);
        
        assertEquals(1, result);
    }
}

✅ 优势一览:

  • 自动随机生成数据库名、用户名、密码
  • 提供 getJdbcUrl() 直接拼好 JDBC 连接串
  • 支持自定义初始化脚本(.withInitScript("schema.sql")
  • 每次测试都是干净的数据库实例,杜绝脏数据

❌ 踩坑提醒:
不要在 @BeforeClass 中复用连接,因为容器可能还没完全就绪。建议每次测试都重新获取连接。


4.2. 浏览器自动化测试(Selenium)

UI 自动化测试常因浏览器版本、驱动不一致而失败。用 BrowserWebDriverContainer,直接在 Docker 里跑 Chrome/Firefox。

@Rule
public BrowserWebDriverContainer<?> chrome = new BrowserWebDriverContainer<>()
    .withCapabilities(new ChromeOptions());

@Test
public void whenNavigatedToPage_thenHeadingIsInThePage() {
    RemoteWebDriver driver = chrome.getWebDriver();
    driver.get("http://example.com");

    String heading = driver.findElement(By.xpath("/html/body/div/h1")).getText();
    
    assertEquals("Example Domain", heading);
}

✅ 关键特性:

  • 内置 docker-selenium 镜像支持
  • getWebDriver() 返回配置好的 RemoteWebDriver 实例
  • 支持 Chrome、Firefox,可传入 ChromeOptions 等自定义配置
  • 视频录制、日志输出等高级功能也支持

📌 适用场景:

  • 接口返回 HTML 的集成测试
  • 微服务架构下的端到端流程验证
  • CI 中运行 UI 回归测试

4.3. 多服务编排:Docker Compose

当测试需要多个相互依赖的服务(如 Web + DB + Redis),用 DockerComposeContainer 更清晰。

先写一个 docker-compose.yml

simpleWebServer:
  image: alpine:3.2
  command: ["/bin/sh", "-c", "while true; do echo 'HTTP/1.1 200 OK\n\nHello World!' | nc -l -p 80; done"]

Java 测试中加载并使用:

@ClassRule
public static DockerComposeContainer compose = 
    new DockerComposeContainer(new File("src/test/resources/test-compose.yml"))
        .withExposedService("simpleWebServer_1", 80);

@Test
public void givenSimpleWebServerContainer_whenGetRequest_thenReturnsResponse() throws Exception {
    String host = compose.getServiceHost("simpleWebServer_1", 80);
    Integer port = compose.getServicePort("simpleWebServer_1", 80);
    String address = "http://" + host + ":" + port;

    String response = simpleGetRequest(address);
    
    assertEquals("Hello World", response);
}

⚠️ 注意事项:

  • 服务名需带编号(如 simpleWebServer_1
  • withExposedService() 显式声明要访问的接口,否则可能无法映射端口
  • 启动时间较长,建议用 @ClassRule 减少开销

5. 总结

TestContainers 是现代 Java 集成测试的利器,核心价值在于:

真实环境:用 Docker 拉起真实中间件,不再是 mock 或 stub
开箱即用:专有容器(PostgreSQL、Selenium)极大简化配置
环境一致:团队、CI 环境完全统一,减少“在我机器上能跑”问题
轻量灵活:支持 Generic、Compose、专用容器,适配各种复杂场景

文中所有代码示例均可在 GitHub 获取:
👉 https://github.com/tech-tutorial/examples/tree/main/testcontainers-demo

📌 建议:
新项目集成测试优先考虑 TestContainers,老项目也可逐步替换 H2、mock 服务,提升测试可信度。


原始标题:Docker Test Containers in Java Tests