首页
/ NocoDB 自动化安装向导的测试体系:noco.sh 的 bats 黄金文件测试与离线验证设计

NocoDB 自动化安装向导的测试体系:noco.sh 的 bats 黄金文件测试与离线验证设计

2026-09-03 16:09:46作者:戚魁泉Nursing

本文围绕 NocoDB 仓库中 docker-compose/1_Auto_Upstall/tests/ 下的测试文档展开,讲清 noco.sh(NocoDB auto-upstall 安装向导)的自动化测试是如何在无 Docker、无网络、任意操作系统上运行的:基于 NOCO_SKIP_PREFLIGHT 环境变量将"文件生成"与"系统预检"解耦,再用 bats 黄金文件(golden-file)快照、标志校验、文档一致性比对和 expect 驱动的交互向导测试四套机制覆盖安装器的全部关键行为。读完本文,你将掌握该测试套件的运行方式、依赖要求、黄金文件再生流程,以及从源码印证的关键设计决策。

一、测试对象与总体设计

被测对象是 noco.sh——NocoDB 的单机安装向导。它的职责是:交互式(或全参数化非交互)地收集域名、Postgres、Redis 与 SSL 配置,然后在 ./nocodb/ 目录生成一套可直接 docker compose up -d 的部署文件(docker-compose.ymldocker.envnocodb/db.jsonupdate.sh.gitignore),并顺带完成预检(Docker 是否存在、80/443 端口是否被占用、SELinux 检测)和拉镜像起栈。

由于安装器同时包含"系统副作用"(检查 Docker、占用端口、docker compose up -d)和"纯文件生成"两类逻辑,测试无法在 CI 里完整执行前者。为此,安装器在源码里内置了一个测试开关:

# noco.sh(第 44 行附近)
NOCO_SKIP_PREFLIGHT="${NOCO_SKIP_PREFLIGHT:-}"   # when set, skip OS/Docker/port checks (testing & re-runs)

这个变量在三个预检/启动入口被检查,设置后即直接返回:

  • check_prereqs()(Docker / Compose / curl 存在性检查)开头:[ -n "$NOCO_SKIP_PREFLIGHT" ] && return 0
  • check_ports()(production 模式下 80/443 端口监听检查)开头同样短路返回;
  • start_stack()(执行 docker compose up -d 的步骤)开头同样短路返回。

这意味着测试只需要 NOCO_SKIP_PREFLIGHT=1 bash noco.sh <flags> 就能在任何 OS 上拿到完整的生成产物,而不需要 Docker、不需要网络——这正是测试文档中所述的核心前提:

安装器的文件生成通过内部 NOCO_SKIP_PREFLIGHT 环境变量与其 OS/Docker/端口预检解耦,因此整套快速测试可在任意操作系统上运行,无需 Docker、无需网络。

二、测试套件覆盖矩阵

测试目录按职责拆分为四个 bats 文件,文档中的覆盖矩阵如下(完整继承自 tests/README.md):

文件 检查内容 需要 Docker
install/generate.bats 按场景对生成的 docker-compose.yml 做黄金文件快照(local、production-ip、production-ssl、external-redis、external-pg ×3、fully-external),外加 db.json / docker.env / 文件权限 / 辅助文件检查
install/validation.bats 非交互标志校验——缺失/不兼容的标志必须快速失败并给出清晰报错;--help 必须成功
install/examples.bats 安装器生成的外部 Postgres db.json 必须与文档示例 docker-compose/examples/ 保持结构同步(jq 归一化后比较) 否(需要 jq
install/interactive.bats 交互向导的 UX 守卫测试,由 expect 驱动。受门控(gated)

三、generate.bats:黄金文件快照测试

generate.bats 是套件的主力。它通过共享辅助函数 lib/helpers.bash 中的 generate <flags...> 把安装器跑进一个隔离的 mktemp -d 临时目录,然后逐项断言产物。

3.1 八个快照场景

每个场景对应一个提交在仓库中的期望输出 golden/<scenario>/docker-compose.yml,测试用 assert_golden <scenario> 做差异比对。场景与命令行参数的对应关系(与 regen-golden.sh 中的场景清单保持一致):

# 场景 1:本地模式(内置 PG + 内置 Redis,端口 8080)
generate --domain=localhost --pg=bundled --redis=bundled

# 场景 2:IP 直连的 production-ip(80 端口,无 traefik)
generate --domain=1.2.3.4 --pg=bundled --redis=bundled

# 场景 3:真实域名 production-ssl(traefik + Let's Encrypt)
generate --domain=demo.example.com --acme-email=ssl@nocodb.com --pg=bundled --redis=bundled

# 场景 4:外部 Redis
generate --domain=localhost --pg=bundled --redis=external --redis-url=redis://localhost:6379

# 场景 5/6/7:外部 PG 的三种 SSL 形态
generate --domain=localhost --pg=external --pg-host=db.example.com --pg-user=nocodb \
  --pg-password=secretpass --pg-ssl=managed --redis=bundled      # 托管 CA(RDS/云 SQL)
generate ... --pg-ssl=none ...                                      # 无 SSL
generate ... --pg-ssl="$(fake_ca)" ...                              # 自定义 CA 证书文件

# 场景 8:PG + Redis 全外部
generate --domain=localhost --pg=external ... --pg-ssl=managed \
  --redis=external --redis-url=redis://redis.example.com:6379

可以对照仓库中提交的两份典型期望输出:

  • golden/local/docker-compose.ymlnocodb + worker + db(postgres:17.10) + redis(redis:7) 四个服务,8080:8080 端口映射,nocodb 服务带 GET /api/v1/health 健康检查,worker 通过 depends_on: condition: service_healthy 等待主服务;
  • golden/production-ssl/docker-compose.yml:在 nocodb 服务上追加 traefik.enable=trueHost(demo.example.com) 路由规则、letsencrypt 证书解析器四个 label,并新增完整的 traefik:v3.6 服务(80/443 入口、HTTP 强制跳转 HTTPS、ACME httpchallenge)。

3.2 密码归一化:唯一动态值的处理

内置 Postgres 的密码由 generate_password() 每次从 /dev/urandom 随机生成,若直接 diff 每次都会失败。helpers.bash 中的 normalize()sed 在比对前把两处随机密码统一替换为占位符 __PASSWORD__

normalize() {
  LC_ALL=C sed -E \
    -e 's/(POSTGRES_PASSWORD: ).*/\1__PASSWORD__/' \
    -e 's/("password"[[:space:]]*:[[:space:]]*")[^"]*(")/\1__PASSWORD__\2/' \
    "$1"
}

对应地,提交的黄金文件中密码位置就是字面量 __PASSWORD__(见 golden/local/docker-compose.yml 第 48 行的 POSTGRES_PASSWORD: __PASSWORD__)。快照测试失败时,bats 输出的是 diff -u 的 unified diff,能精确定位改动行。

3.3 除快照之外的产物断言

generate.bats 还包含一组针对性断言,覆盖安装器生成的其他文件与行为:

  • db.json 内容:内置 PG 时 clientpghostdb(compose 服务名)、port5432databasenocodb,且不含 ssl 块;外部 PG 无 SSL 时 host 指向外部主机且无 ssl 块;外部 PG 自定义 CA 时 db.json 内嵌 "rejectUnauthorized": trueBEGIN CERTIFICATE(证书内容被 json_escape 后逐字嵌入,对应 noco.shgenerate_db_json() 的 custom 分支)。
  • 密码一致性:内置 PG 场景下,从 docker-compose.ymlPOSTGRES_PASSWORDdb.json"password" 提取的密码必须完全相同、长度 ≥ 24、且仅含字母数字——保证 compose 里的数据库与 NocoDB 的连接串不会因生成时序错位而失配。
  • docker.env 固定配置块NC_DB_JSON_FILE=/usr/app/data/db.jsonNC_SECURE_ATTACHMENTS=trueNC_DISABLE_MUX=true 必须逐行存在;NC_REDIS_URL 在内置/外部 Redis 下分别取 redis://redis:6379 与给定 URL;NC_SITE_URL 随模式变化——local 模式为 http://localhost:8080、production-ip 为 http://<ip>、production 为 https://<domain>(对应 generate_env() 中 site_url 的三分支逻辑,该 URL 是邮件链接、Webhook 与 OAuth 回调正确解析所必需的)。
  • 安全与辅助文件docker.env 权限必须为 600(对应 tighten_perms()chmod 600,且脚本开头用 umask 077 从创建时就限制属主权限);update.sh 必须可执行;.gitignore 必须排除 docker.envnocodb/db.json 两个含凭据的文件。
  • 门控的 Docker 冒烟compose validates with docker compose config 测试仅在 TEST_DOCKER=1 时运行,用真实的 docker compose config 校验生成物的 YAML 合法性,否则自动 skip。

四、validation.bats:非交互标志的 fail-fast 契约

validation.bats 守护的是运维契约:非交互模式下缺参必须立即以退出码 1 失败并打印可操作的错误信息,而不是挂起在提示符上。它通过 run_noco() 包装器(run env NOCO_SKIP_PREFLIGHT=1 bash noco.sh "$@")捕获退出码与输出,覆盖的失败用例包括:

用例 期望输出片段
--pg --pg=bundled or --pg=external is required
--pg=external--pg-host / --pg-user / --pg-password 各自的 is required when --pg=external 提示
--redis --redis=bundled or --redis=external is required
--redis=external--redis-url --redis-url is required when --redis=external
未知标志 --bogus Unknown flag
production 域名缺 --acme-email --acme-email is required
--pg-ssl=/no/such/ca.pem 指向不存在的 CA CA file not found
--pg-ssl=mananged(拼写错误的值) 同样报 CA file not found,而非静默写出空 CA
已移除的子命令 upgrade 提示 no longer a subcommand,引导使用 docker compose / update.sh 的新工作流
--help 退出码 0 且打印 Non-interactive flags 用法

其中"拼写错误的 --pg-ssl 值必须失败"这条尤其值得注意:在 noco.shparse_flags() 中,--pg-ssl=managed--pg-ssl=none 是精确分支,其余任何值都会落入 --pg-ssl=*) 分支被当作自定义 CA 路径处理,随后由 validate_non_interactive() 检查文件存在性——这个"兜底分支 + 文件存在校验"的组合把误拼写变成了显式错误,而不是生成一份缺失 CA 的坏配置。

五、examples.bats:安装器与文档示例的结构一致性

examples.bats 解决的是一个文档漂移问题:仓库在 docker-compose/examples/ 下提交了三份手写的部署示例(managed-postgrespostgres-private-caexternal-postgres-and-redis),它们各自带有一份 nocodb/db.json。这些示例与安装器实际生成的 db.json 描述的是同一批部署形态,一旦安装器生成逻辑变化而示例没更新,用户照着文档配置就会踩坑。

测试的比对方式是"结构同形"而非字节相同——用 jq 把所有字符串值抹平成常量 "X",只比较键结构与 ssl 块的形状:

norm_json() { jq -S 'walk(if type == "string" then "X" else . end)' "$1"; }

assert_same_structure() {
  diff <(norm_json "$1") <(norm_json "$2")
}

三个用例分别把安装器生成的 db.json 与对应示例比对:

该文件在 jq 缺失时自动 skip,且文件头注释明确说明不要求字节一致——示例的 docker.env 合法地与安装器输出不同,只有 db.json 的结构需要保持一致。

六、interactive.bats:expect 驱动的向导 UX 守卫

interactive.bats 是唯一门控(gated)的用例:它不测标志驱动的生成路径,而是用 expects/install/interactive.sh 这份 expect 脚本真实驱动一遍交互式向导:

set env(PATH) "$here/../../mocks:$env(PATH)"
set env(NOCO_SKIP_PREFLIGHT) "1"

spawn bash "$here/../../../noco.sh"

expect "Domain or IP*" ; send "localhost\r"   # 域名 → local 模式
expect ">*"            ; send "1\r"            # Postgres → 1) Bundled
expect ">*"            ; send "1\r"            # Redis    → 1) Bundled
expect "Proceed?*"     ; send "\r"              # 确认摘要(默认 Y)
expect eof

两个细节保证它离线可跑:

  1. curl mock:安装器用 curl -s --max-time 3 https://api.ipify.org 探测公网 IP 作为域名默认值,expect 脚本把 mocks/ 目录前置到 PATH,而 mocks/curl 只是一行 exit 0 的桩,从而切掉唯一的网络调用;
  2. NOCO_SKIP_PREFLIGHT=1:跳过 Docker 检查与 docker compose up -d,expect 脚本只验证交互路径能把配置生成出来。

测试断言生成的 compose 中存在 '8080:8080' 端口映射、image: postgresimage: redis 两个内置服务。运行方式为:

TEST_INTERACTIVE=1 bats install/interactive.bats   # 需要额外安装 expect

七、运行方式与依赖

docker-compose/1_Auto_Upstall/tests 目录下:

bats install/                       # 运行整套快速测试
bats install/generate.bats          # 只跑单个文件

TEST_DOCKER=1      bats install/generate.bats      # 附加运行 `docker compose config` 校验
TEST_INTERACTIVE=1 bats install/interactive.bats   # 运行 expect 驱动的向导测试

依赖:bats(整套)、jq(仅 examples.bats,缺失时 skip)、expect(仅门控的 interactive 测试,缺失时 skip)。由于全部生成运行都设置 NOCO_SKIP_PREFLIGHT=1 且网络调用被 mock,整套快速测试可在任意 OS、无 Docker、无网络的环境(包括 CI)中执行。

另外,helpers.bashnoco_scratch() 刻意不依赖 $BATS_TEST_TMPDIR——该变量自 bats 1.4.0 才存在,而 CI 所用发行版自带的 bats 1.2.1 下它是空值,因此辅助函数改用 mktemp -d 自建隔离目录并在 teardown 中清理。

八、黄金文件再生流程

有意修改了 noco.sh 的 compose 生成逻辑(而非修复回归)时,测试不应被当作噪音绕过,而是走再生流程:

./lib/regen-golden.sh
git diff golden/        # 确认改动符合预期后再提交

regen-golden.sh 以与 generate.bats 完全相同的八组场景参数重跑安装器,把归一化(密码替换为 __PASSWORD__)后的 docker-compose.yml 覆盖写回 golden/<scenario>/;脚本头部的注释明确要求只在有意变更时运行,并且场景清单需与 generate.bats 保持同步。自定义 CA 场景使用运行时生成的临时 PEM 文件(与 helpers.bashfake_ca() 内容一致),因为仓库 .gitignore 排除了 *.pem

九、小结:这套测试设计的可复用点

从源码结构看,1_Auto_Upstall/tests/ 展示了一套针对"带系统副作用的生成型脚本"的可移植测试范式:

  1. 测试开关内建于被测脚本NOCO_SKIP_PREFLIGHTcheck_prereqs / check_ports / start_stack 三处短路,使生成逻辑与系统环境彻底解耦,测试矩阵因此可以在无 Docker、无网络的 CI 上全量运行;
  2. 黄金文件 + 动态值归一化:唯一随机值(Postgres 密码)经 sed 归一为 __PASSWORD__ 后做 diff -u 快照,失败信息即精确 diff;再生流程(regen-golden.sh + git diff golden/ 审查)保证快照更新始终显式、可审计;
  3. fail-fast 契约测试:把每条错误信息文案固化为断言,防止提示退化或误拼写参数静默产出坏配置(--pg-ssl 兜底分支的教训);
  4. 结构一致性守卫:用 jq 抹平字符串值后比对结构,让"安装器输出"与"文档示例"(docker-compose/examples/ 下三份 db.json)在仓库内持续对齐,防止文档漂移;
  5. 门控分层TEST_DOCKER=1TEST_INTERACTIVE=1jq/expect 存在性检查让增强型测试按需启用,基础套件保持"零外部依赖 + 离线"的快速反馈。

对仓库使用者而言,该目录也是理解 noco.sh 行为契约的最佳入口:每个 @test 都精确对应安装器的一段生成或校验逻辑,配合 安装向导文档 可以完整掌握从交互提问到产物落盘的全过程。

登录后查看全文
热门项目推荐
相关项目推荐