Skip to content

测试与故障验证 ​

问题:单元测试全绿,为什么上线后重复执行 ​

mock Repository 始终返回成功,不会模拟两个事务同时认领;mock 消息发送成功,也不会模拟消息到达但 ACK 丢失。测试通过只证明所覆盖条件成立,不能自动推广到所有边界。

从业务不变量开始设计测试:“重复请求不创建第二个 Task”“旧 claim 无法确认新租约”“提交后崩溃不会丢待发事实”。这些比测某个内部方法被调用几次更稳定。

原理:让测试接近需要证明的机制 ​

领域规则与调度纯函数用单元测试;数据库唯一约束、锁和原子写入要用真实数据库集成测试;接口结构用契约测试;跨进程通信、认证和恢复用端到端测试;耐久工作流升级还需历史 Replay 验证。

每个测试写清前置状态、触发、可观察证据和允许差异。最终一致性不要仅 sleep 固定秒数;在有截止时间的范围内轮询业务条件,同时在失败时保存诊断证据。

最小例子:测试故障窗口 ​

text
Outbox 故障测试:
1. 提交业务与事件,确认两条持久记录都存在。
2. 发送成功后模拟记录确认失败。
3. 推进测试时钟超过租约,重新认领同一事件 ID。
4. 消费端处理两次投递。
5. 断言业务效果只有一次,事件最终确认,原始 ID 未变化。

如果只断言“Publish 被调用两次”,没有验证消费端事务,则不能证明业务幂等。测试时钟适合纯逻辑,真实数据库的时间条件还需检查实际 SQL。

方案比较 ​

测试能证明不能单独证明
单元规则、排序、状态转换真实事务与网络
集成SQL、驱动、锁、唯一约束全链路配置
契约wire shape、校验与兼容业务状态正确
E2E / 故障多组件推进和恢复全部并发组合
Replay历史工作流兼容外部副作用幂等

mock 适合隔离协作者的错误路径,但过度模拟会把错误假设写进测试。并发测试要控制竞争窗口,避免偶尔跑到某种调度顺序才“碰巧通过”。

真实案例:生成门禁与跨组件验证 ​

backend Makefile 提供单元、race、Schema、配置、架构和生成检查。根 Workbench E2E 通过独立 Compose 项目验证多组件交互。源契约变化后需要再生成并检查 drift,不能只验证 Go 编译。

已核对的实现 · 本地代码快照

生成与工程门禁 backend · c6dbe05c

Makefile · 第 37–83 行
符号:generate / check · 核对日期 2026-10-02
来源与提交版本一致

generate:
	$(GO) tool oapi-codegen -config contracts/oapi-codegen/public.yaml contracts/openapi/public/v1/openapi.yaml
	$(GO) tool oapi-codegen -config contracts/oapi-codegen/worker.yaml contracts/openapi/worker/v1/openapi.yaml
	$(GO) tool oapi-codegen -config contracts/oapi-codegen/management.yaml contracts/openapi/management/v1/openapi.yaml
	$(GO) tool oapi-codegen -config contracts/oapi-codegen/callback.yaml contracts/openapi/callback/v1/openapi.yaml
	$(GO) tool oapi-codegen -config contracts/oapi-codegen/resource-provider.yaml contracts/openapi/resource-provider/v1/openapi.yaml
	$(GO) tool sqlc generate
	$(GO) run ./tools/sqlcpostprocess

migration-validate:
	$(GO) tool goose -dir server/db/migrations validate
	$(GO) tool goose -dir worker/db/migrations validate

generate-check: generate
	git diff --exit-code -- contracts/gen/go server/internal/adapters/outbound/postgres/pgqueries worker/internal/adapters/outbound/sqlite/sqlitequeries

schema-check:
	$(GO) test ./contracts/...

config-check:
	TDP_SERVER_OIDC_CLIENT_SECRET=validation TDP_SERVER_DATABASE_URL=postgres://tdp:validation@localhost/tdp TDP_SERVER_NATS_URL=nats://localhost:4222 TDP_SERVER_DATA_ENCRYPTION_KEY=BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc= $(GO) run ./server/cmd/server config validate --config configs/examples/server.yaml
	$(GO) run ./worker/cmd/worker config validate --config configs/examples/worker.yaml

architecture-check:
	$(GO) run ./tools/archcheck

api-compat:
	@test -n "$(BASE_OPENAPI_DIR)" || (echo "BASE_OPENAPI_DIR is required"; exit 2)
	$(GO) tool oasdiff breaking $(BASE_OPENAPI_DIR)/public/v1/openapi.yaml contracts/openapi/public/v1/openapi.yaml
	$(GO) tool oasdiff breaking $(BASE_OPENAPI_DIR)/worker/v1/openapi.yaml contracts/openapi/worker/v1/openapi.yaml

vuln:
	$(GO) tool govulncheck ./...

license-check:
	$(GO) tool go-licenses check ./server/cmd/server ./worker/cmd/worker \
		--ignore=tdp \
		--ignore=github.com/nexus-rpc/nexus-proto-annotations/go/nexusannotations/v1

check: fmt-check vet test schema-check config-check architecture-check migration-validate

clean:
	$(GO) clean ./...
	rm -f $(BIN_DIR)/server $(BIN_DIR)/worker
	rm -f $(BIN_DIR)/server-linux-amd64 $(BIN_DIR)/worker-linux-amd64
	rm -f $(BIN_DIR)/worker-linux-arm64 $(BIN_DIR)/worker-windows-amd64.exe

片段展示核对时的源码;完整文件指纹用于检测后续变化。这里的路径用于定位,不要求手机访问源码仓库。

本地栈与验证入口 workbench · 514f5a00

Makefile · 第 19–55 行
符号:check / up / e2e · 核对日期 2026-10-02
来源与提交版本一致

check:
	$(COMPOSE) -f compose.yaml config --quiet
	$(COMPOSE) -f compose.yaml -f compose.dev.yaml config --quiet
	$(COMPOSE) -f compose.yaml -f compose.dev.yaml -f compose.demo.yaml config --quiet
	$(COMPOSE) -f compose.yaml -f compose.e2e.yaml config --quiet
	$(COMPOSE) -f compose.yaml -f compose.demo.yaml config --quiet
	python3 scripts/check-doc-links.py

docs-check:
	python3 scripts/check-doc-links.py

ps:
	$(LOCAL_COMPOSE) ps -a

up:
	$(LOCAL_COMPOSE) up -d --build

down:
	$(LOCAL_COMPOSE) down

logs:
	$(LOCAL_COMPOSE) logs -f

e2e:
	TDP_E2E_PROJECT=$(E2E_PROJECT) ./scripts/e2e.sh

e2e-down:
	$(COMPOSE) -p $(E2E_PROJECT) -f compose.yaml -f compose.e2e.yaml $(E2E_EXTRA_COMPOSE) --profile test down

fault-test:
	TDP_E2E_PROJECT=$(E2E_PROJECT) ./scripts/fault-test.sh

demo-up:
	$(LOCAL_COMPOSE) -f compose.demo.yaml up -d --build

demo-down:
	$(LOCAL_COMPOSE) -f compose.demo.yaml down

片段展示核对时的源码;完整文件指纹用于检测后续变化。这里的路径用于定位,不要求手机访问源码仓库。

本学习站自己的测试验证教学模型的故障结果和移动交互,不等同于再次验收业务系统。没有改变业务代码时,不应启动完整业务 E2E 来替代网站测试。

失败与边界:环境失败不是逻辑证据 ​

无法绑定端口、下载依赖失败或沙箱禁止缓存写入,要与断言失败区分。将 unchanged tests 在合适环境重跑后才能下结论。也不能因环境限制就报告业务已通过。

测试清理保留数据库与用户数据。集成测试使用隔离资源;故障注入必须作用于测试栈,不把学习按钮连到真实系统。

迁移练习与参考答案 ​

练习:新增消息认领 SQL,单元测试已覆盖失败和重试,还需要什么?

参考答案:真实数据库测试验证并发认领不重叠、过期记录可回收、旧 claim 更新失败、事务中断不丢事实;集成消息测试验证重复投递与 ACK 窗口;涉及跨组件路径时做 E2E。保留最终数据库证据,不能只看日志说“看起来正常”。

继续阅读:交付与发布、可观测性。

理解原理 · 分析取舍 · 用真实代码检验