bigywater.tech
Java · 379

Testcontainers:用真实依赖做集成测试

Testcontainers 的价值在于用真实依赖替代进程内替身。这篇在真实的 Docker 上验证:先要解决 Docker 29 与旧版 Testcontainers 的 API 版本不兼容,再量容器的启动成本、Ryuk 的清理行为、每个测试方法一个容器的代价,最后用真实 PostgreSQL 对照 H2。

HTTP 400
Testcontainers 1.20.4 请求 /v1.32/info,Docker 29.8.2(最低 API 1.40)返回的状态码;-Dapi.version=1.44 或 1.21.4 可解决
719 ms
启动一个 postgres:16-alpine 容器的中位数(第一次含 Ryuk 1222 ms);每个测试方法一个容器 9.4 s,static 容器 1.8 s
4 / 12
合法的 PostgreSQL 语句被 H2 2.3 拒绝的数量(ON CONFLICT DO UPDATE、RETURNING、jsonb、@>)

Docker 29 与 API 版本

实测 · 本机执行输出
  docker 服务端(GET /version):Version 29.8.2 / ApiVersion 1.56 / MinAPIVersion 1.40
  GET /v1.32/info 返回 HTTP 400(1.20.4 的 docker-java 默认请求这个版本),GET /v1.44/info 返回 HTTP 200
  Testcontainers 1.20.4,没有任何设置:PgIT 用时 0.4 s;│ └─ PgIT ✘ Could not find a valid Docker environment. Please see logs and check configuration / [ 0 tests successful ]
  1.20.4 + 环境变量 DOCKER_API_VERSION=1.44:PgIT 用时 0.5 s;│ └─ PgIT ✘ Could not find a valid Docker environment. Please see logs and check configuration / [ 0 tests successful ]
  1.20.4 + -Dapi.version=1.44:PgIT 用时 1.7 s;│ └─ PgIT ✔ / [ 1 tests successful ]
  Testcontainers 1.21.3,没有任何设置:PgIT 用时 0.5 s;│ └─ PgIT ✘ Could not find a valid Docker environment. Please see logs and check configuration / [ 0 tests successful ]
  Testcontainers 1.21.4,没有任何设置:PgIT 用时 1.7 s;│ └─ PgIT ✔ / [ 1 tests successful ]

一个真实的容器

实测 · 本机执行输出
  pg.start() 用时 1225 ms(含连接 Docker、启动 Ryuk 容器、启动 postgres 并等到日志里出现 ready)
  镜像 postgres:16-alpine,容器内 5432 映射到宿主机的 随机高位端口,host localhost,默认库名 / 用户:test / test
  select version() = PostgreSQL 16.15 on aarch64-unknown-
  测试运行期间 docker ps:postgres:16-alpine | Up Less than a second
  测试运行期间 docker ps:testcontainers/ryuk:0.11.0 | Up Less than a second

Ryuk 与容器泄漏

实测 · 本机执行输出
  Ryuk 开启(默认),JVM 正常退出(没有调用 stop):JVM 结束时 docker ps -a 里有 2 个容器;10.4 秒后全部消失
  Ryuk 开启(默认),JVM 被 kill -9:JVM 结束时 docker ps -a 里有 2 个容器;10.5 秒后全部消失
  TESTCONTAINERS_RYUK_DISABLED=true,JVM 正常退出:JVM 结束时 docker ps -a 里有 0 个容器;0.0 秒后全部消失
  TESTCONTAINERS_RYUK_DISABLED=true,JVM 被 kill -9:JVM 结束时 docker ps -a 里有 1 个容器;45 秒后还剩 1 个:postgres:16-alpine | Up 46 seconds

容器的成本

实测 · 本机执行输出
  第 1 次:start 1222 ms(含第一次连接 Docker 与启动 Ryuk),第一个连接加查询 113 ms,stop 127 ms
  第 2 次:start 714 ms(Ryuk 已在运行),第一个连接加查询 18 ms,stop 111 ms
  第 3 次:start 721 ms(Ryuk 已在运行),第一个连接加查询 18 ms,stop 120 ms
  第 4 次:start 704 ms(Ryuk 已在运行),第一个连接加查询 12 ms,stop 119 ms
  第 5 次:start 719 ms(Ryuk 已在运行),第一个连接加查询 13 ms,stop 123 ms
  start 的中位数 719 ms(后 4 次 704–721 ms),stop 的中位数 120 ms
  同一个容器里(含 1000 行的 tpl 模板库)连续 20 次 CREATE DATABASE ... TEMPLATE tpl(通过 JDBC,不含进程启动)中位数 6 ms(最小 5、最大 16),DROP DATABASE 中位数 2 ms
  PerMethod:10 个测试方法,@Container 是非静态字段(每个测试方法一个容器):总用时 9.4 s,创建的容器(按镜像):{'postgres:16-alpine': 11, 'testcontainers/ryuk:0.11.0': 1};[ 10 tests successful ] / [ 0 tests failed ]
  Shared:同样 10 个测试方法,@Container 是 static(整个类一个容器):总用时 1.8 s,创建的容器(按镜像):{'testcontainers/ryuk:0.11.0': 1, 'postgres:16-alpine': 1};[ 10 tests successful ] / [ 0 tests failed ]

没有 Docker 时

实测 · 本机执行输出
  [PgIT] 用时 0.4 s
      ├─ JUnit Platform Suite ✔
      ├─ JUnit Jupiter ✔
      │ └─ PgIT ✘ Could not find a valid Docker environment. Please see logs and check configuration
      └─ JUnit Vintage ✔
      => java.lang.IllegalStateException: Could not find a valid Docker environment. Please see logs and check configuration
      [ 4 containers found ]
      [ 0 containers skipped ]
      [ 3 containers successful ]
      [ 1 containers failed ]
      [ 1 tests found ]
      [ 0 tests skipped ]
      [ 0 tests successful ]
      [ 0 tests failed ]
  --- 同一个类加 @Testcontainers(disabledWithoutDocker = true),并多一个根本不需要容器的 plain() 测试;对照一个没有容器的 Plain 类
  [PgSkip] 用时 0.3 s
      ├─ JUnit Platform Suite ✔
      ├─ JUnit Jupiter ✔
      │ └─ PgSkip ↷ disabledWithoutDocker is true and Docker is not available
      └─ JUnit Vintage ✔
      [ 4 containers found ]
      [ 1 containers skipped ]
      [ 3 containers successful ]
      [ 0 containers failed ]
      [ 2 tests found ]
      [ 2 tests skipped ]
      [ 0 tests successful ]
      [ 0 tests failed ]
  [Plain] 用时 0.3 s
      ├─ JUnit Platform Suite ✔
      ├─ JUnit Jupiter ✔
      │ └─ Plain ✔
      │ └─ plain() ✔
      └─ JUnit Vintage ✔
      [ 4 containers found ]
      [ 0 containers skipped ]
      [ 4 containers successful ]
      [ 0 containers failed ]
      [ 1 tests found ]
      [ 0 tests skipped ]
      [ 1 tests successful ]
      [ 0 tests failed ]

H2 与真实数据库

实测 · 本机执行输出
  [ON CONFLICT DO UPDATE]
      H2 2.3 默认模式:失败 [42000] Syntax error in SQL statement "INSERT INTO t(id, name) VALUES (1, 'x') [*]ON CONFLICT (id) DO UPDATE SET name = 'y'"
      H2 2.3 MODE=PostgreSQL:失败 [42000] Syntax error in SQL statement "INSERT INTO t(id, name) VALUES (1, 'x') [*]ON CONFLICT (id) DO UPDATE SET name = 'y'"
      PostgreSQL 16 真实:成功(更新 1)
  [RETURNING]
      H2 2.3 默认模式:失败 [42000] Syntax error in SQL statement "INSERT INTO t(id, name) VALUES (9, 'r') [*]RETURNING id"
      H2 2.3 MODE=PostgreSQL:失败 [42000] Syntax error in SQL statement "INSERT INTO t(id, name) VALUES (9, 'r') [*]RETURNING id"
      PostgreSQL 16 真实:成功 9
  [jsonb 运算符]
      H2 2.3 默认模式:失败 [HY004] Unknown data type: "JSONB"
      H2 2.3 MODE=PostgreSQL:失败 [42000] Syntax error in SQL statement "SELECT '{""a"":1}'::jsonb -[*]>> 'a'"
      PostgreSQL 16 真实:成功 1
  [generate_series]
      H2 2.3 默认模式:成功 1,2,3
      H2 2.3 MODE=PostgreSQL:成功 1,2,3
      PostgreSQL 16 真实:成功 1,2,3
  [数组包含 @>]
      H2 2.3 默认模式:失败 [42000] Syntax error in SQL statement "SELECT ARRAY[1,2,3] [*]@> ARRAY[2]"
      H2 2.3 MODE=PostgreSQL:失败 [42000] Syntax error in SQL statement "SELECT ARRAY[1,2,3] [*]@> ARRAY[2]"
      PostgreSQL 16 真实:成功 true
  [text 列 = 整数]
      H2 2.3 默认模式:失败 [22018] Data conversion error converting "CHARACTER VARYING to DECFLOAT"
      H2 2.3 MODE=PostgreSQL:失败 [22018] Data conversion error converting "CHARACTER VARYING to DECFLOAT"
      PostgreSQL 16 真实:失败 [42883] ERROR: operator does not exist: character varying = integer
  [FOR UPDATE SKIP LOCKED]
      H2 2.3 默认模式:成功 1,2,3
      H2 2.3 MODE=PostgreSQL:成功 1,2,3
      PostgreSQL 16 真实:成功 1,2,3
  [DISTINCT ON]
      H2 2.3 默认模式:成功 4,1,3
      H2 2.3 MODE=PostgreSQL:成功 4,1,3
      PostgreSQL 16 真实:成功 4,1,3
  [to_char(date)]
      H2 2.3 默认模式:成功 2026-03-08
      H2 2.3 MODE=PostgreSQL:成功 2026-03-08
      PostgreSQL 16 真实:成功 2026-03-08
  [字符串比较 'a' < 'B']
      H2 2.3 默认模式:成功 false
      H2 2.3 MODE=PostgreSQL:成功 false
      PostgreSQL 16 真实:成功 false
  [除零]
      H2 2.3 默认模式:失败 [22012] Division by zero: "1"
      H2 2.3 MODE=PostgreSQL:失败 [22012] Division by zero: "1"
      PostgreSQL 16 真实:失败 [22012] ERROR: division by zero
  [整数溢出 2147483647 + 1]
      H2 2.3 默认模式:失败 [22003] Numeric value out of range: "2147483648"
      H2 2.3 MODE=PostgreSQL:失败 [22003] Numeric value out of range: "2147483648"
      PostgreSQL 16 真实:失败 [22003] ERROR: integer out of range

事务里的 DDL

实测 · 本机执行输出
  H2 2.3 默认模式:CREATE TABLE 之后 ROLLBACK → 回滚之后表还在(DDL 没有被回滚)
  H2 2.3 MODE=PostgreSQL:CREATE TABLE 之后 ROLLBACK → 回滚之后表还在(DDL 没有被回滚)
  PostgreSQL 16 真实:CREATE TABLE 之后 ROLLBACK → 回滚之后表不存在(DDL 被回滚)

不用容器的隔离

实测 · 本机执行输出
  模板库里有 orders 表,1000 行;连续 20 次 CREATE DATABASE ... TEMPLATE tpl 的中位数 57 ms(最小 55,最大 59);DROP DATABASE 中位数 30 ms(含 psql 进程启动)
  对照:一次 psql -c "SELECT 1"(只有进程启动与连接)约 18 ms
  共享一个库:测试类 A 插入 1 行后没清理,测试类 B 执行 SELECT count(*) FROM orders WHERE status = 'A-left' 看到 1 行;各自一个克隆库:A 在 cla 里插入同样的行,B 在 clb 里看到 0 行(A 自己看到 1 行)

分步演示

Testcontainers
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12;Testcontainers 1.20.4(另测 1.21.3 / 1.21.4)、JUnit console-standalone 1.11.3、H2 2.3.232、PostgreSQL JDBC 42.7.4;Docker Desktop 29.8.2(arm64,容器镜像 postgres:16-alpine 与 testcontainers/ryuk 因为 Docker 守护进程没有配置代理、拉不到 Docker Hub,是在宿主机经本地代理下载后用 docker load 装入的);六至八的真实数据库是本机 Homebrew 的 PostgreSQL 16.15(独立数据目录、端口 9501);第五节的“没有 Docker”是把 user.home 指向空目录模拟的,不是停掉 Docker
    • 没有验证:Linux CI 与 Docker-in-Docker 上的行为、容器复用(reuse)、Docker Compose 模块、镜像拉取成本(镜像是预先装好的)、其它容器(Kafka / Redis 等)、Testcontainers 2.x;PerMethod 多出的第 11 个 postgres 容器没有查明原因;H2 与 PostgreSQL 的比较只覆盖了 12 条 SQL 与 1 个事务场景,不代表兼容性的全貌
    京ICP备2026067502号-1京公网安备11010502063487号