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