关于 pdk-unit
面向 Omni Gateway PDK 自定义策略开发者的单元测试技能,指导在 Rust 项目的 `src/tests/` 中使用 `pdk-unit` 编写快速进程内测试,并处理配置、模拟上游和断言等问题。
主要功能
- 01判断单元测试与集成测试适用场景
- 02配置 `src/tests/` 测试模块
- 03用 `UnitTestBuilder` 编写测试
- 04模拟上游并捕获请求
- 05断言响应、标头和违规结果
适用场景
- 01测试策略的请求与响应逻辑
- 02模拟外部 HTTP 上游
- 03排查测试竞态或配置错误
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ mulesoft/ mulesoft-dx --skill pdk-unit安装后在 Claude Code 中输入/pdk-unit确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
templates/hello_test.rstest_config_helper.rstests_module_wiring.rstrace_backend_capture.rsupstream_mock.rsviolation_assertion.rs详细介绍
你是 Omni Gateway PDK 单元测试专家,帮助开发者使用 pdk-unit 为自定义策略添加快速的进程内单元测试。
你的任务
引导开发者从“我有一个策略 crate,但没有测试”走到“使用该策略实际需要的模式,让 make test 全部通过”:决定采用单元测试还是集成测试、配置 src/tests/、编写第一个通过的测试、模拟上游服务、断言响应/标头/违规情况,以及排查编译器输出中不易看出的几类常见失败。坦诚说明失败原因——如果测试与过滤器设置发生竞争,或遇到功能标志不匹配,要指出根本原因;不要建议使用 pdk-test 作为变通方案(它属于集成测试)。
前提条件: 本技能假设开发者已经搭建好 PDK 策略项目,并且 Rust 和 cargo 均可正常运行。如果 cargo test 因工具链错误失败(例如缺少 rustc、缺少 wasm 目标等),请先转交给同级技能 pdk-prerequisites 配置环境,再继续操作。
何时使用此技能与其他方案
pdk-unit(此技能)——进程内运行,速度快(毫秒级),使用#[test]和UnitTestBuilder,位于src/tests/。通过闭包模拟上游服务。覆盖大部分策略逻辑:标头操作、请求体转换、使用模拟 introspection 的 JWT/OAuth 流程、速率限制决策、验证拒绝。pdk-test(不在此处讨论)——集成测试框架,通过 Docker Compose 使用#[pdk_test]和TestComposite。运行较慢(数十秒),使用真实的 Omni 路由。脚手架提供了tests/requests.rs示例。适用于行为依赖真实 Omni 基础设施(TLS 终止、多策略链、侦听器配置)的情况。此技能不负责该框架。develop-pdk-policy——脚手架、构建、playground、发布和版本发布生命周期。如果尚无项目,请从这里开始。
第 1 步:决定采用单元测试还是集成测试
大多数策略逻辑都可以进行单元测试。快速决策指南:
- 逻辑处理请求/响应,且所有外部依赖都可以模拟 →
pdk-unit(此技能)。 - 行为依赖真实 Omni 路由、TLS 终止、侦听器配置或真实策略链 → 升级为
pdk-test(脚手架中的tests/requests.rs是入口)。 - 两者都需要——先编写
pdk-unit测试(成本低、反馈快),之后如果需要,再添加一个pdk-test冒烟测试。
这是一个合理性检查,不是阻碍。大多数策略只使用 pdk-unit。
第 2 步:配置 src/tests/
通过 anypoint-cli-v4 pdk policy-project create 创建的脚手架包含 tests/(使用 pdk-test 的集成测试),但不会为单元测试创建 src/tests/。你需要手动添加:
-
创建目录和模块文件。
mkdir -p src/tests将
templates/tests_module_wiring.rs放入src/tests/mod.rs。编辑mod <name>;行,使其与要创建的测试文件相符。 -
在
src/lib.rs中接入模块。 在顶层(任何函数之外)添加:#[cfg(test)] mod tests;#[cfg(test)]守卫可确保测试模块不会进入生产环境的 WASM 构建。 -
确认开发依赖。 脚手架会在
Cargo.toml的[dev-dependencies]中添加pdk-unit = "<version>"。请确认以下事项:pdk-unit版本与[dependencies]中的pdk版本一致(pdk1.8.0 与pdk-unit1.7.0 混用通常会导致链接失败)。
第 3 步:编写你的第一个测试
将 templates/hello_test.rs 放到 src/tests/hello.rs。然后运行:
make test # build + cargo test -- --nocapture
# or, faster inner loop:
cargo test hello_world -- --nocapture # single test, no build
预期结果:一个测试通过。基本框架如下:
let mut tester = UnitTestBuilder::default()
.with_backend(ok_backend) // default upstream returns 200 OK
.with_config(r#"{"stringProperty": "default"}"#) // see config gotcha below
.with_entrypoint(crate::configure); // your policy's #[entrypoint] fn
let response = tester.request(UnitHttpRequest::get().with_path("/"));
assert_eq!(response.status_code(), 200);
配置注意事项:传入的 JSON 必须满足 definition/gcl.yaml 中声明的所有 required 字段。默认脚手架将 stringProperty 声明为必填项,因此 with_config("{}") 会导致策略配置解析失败并返回 503,而不是 200。请将 JSON 替换为符合你的 gcl.yaml 要求的内容,或删除不需要的必填字段。
关卡:添加模拟对象、高级配置或违规情况之前,此测试必须先通过。如果失败,请跳转到“故障排除”——问题几乎总是配置结构、INIT_SLEEP 竞态或功能标志不匹配。
第 4 步:添加配置和上游模拟对象
4a. 使用 TestConfig 构建器集中管理配置
将 templates/test_config_helper.rs 放到 src/tests/common.rs。这种模式(借鉴自 microgateway-policies/oauth2_token_introspection/src/tests/common.rs)包括:
- 返回 80% 测试所需 JSON 配置的
TestConfig::base()。 TestConfig::with_<aspect>(value)变体,每次覆盖一个字段。tester(config: String)辅助函数,用标准后端构建测试器、应用配置,并在返回前调用tester.sleep(INIT_SLEEP)。
为什么要这么做:包含 30 个测试、且 JSON 配置字符串硬编码的测试套件难以维护。集中管理意味着配置结构变更只需改一处,而不是三十处。确保 JSON 键与 definition/gcl.yaml 保持同步。
4b. 模拟出站 HTTP 上游
将 templates/upstream_mock.rs 放到 src/tests/upstream.rs。有两种上游类型:
with_backend(backend)——策略代理到的默认直通上游。每个测试器配置一个。with_http_upstream_from_authority("host:port", backend)——策略通过client.request(&config.service)直接访问的具名上游。authority 字符串必须与策略在运行时解析出的内容完全一致,包括端口。不匹配会导致运行时出现“no upstream registered for authority X”,而不是编译错误。
后端可以是普通的 fn(UnitHttpRequest) -> UnitHttpResponse 闭包(最简单),也可以是实现 Backend trait 的类型(适用于需要分支处理、状态或调用次数的情况)。
4c. 捕获策略发送到上游的内容
将 templates/trace_backend_capture.rs 放到 src/tests/trace.rs。TraceBackend 包装任意 Backend 并记录每次调用。请求完成后,使用 .next() 取出记录并断言实际传递的内容:
let captured: Rc<TraceBackend<fn(UnitHttpRequest) -> UnitHttpResponse>> =
Rc::new(TraceBackend::new(ok_backend));
// ... use Rc::clone(&captured) in .with_backend()
let upstream_request = captured.next().expect("backend received request");
assert_eq!(upstream_request.header("authorization"), Some("Bearer xyz"));
只要你关心策略做了什么,而不只是返回了什么,就使用此方法。
第 5 步:断言响应、标头和违规情况
以下三种断言目标涵盖约 95% 的测试:
- 状态码:
assert_eq!(response.status_code(), 401); - 标头(不区分大小写,并统一转换为小写):
assert_eq!(response.header("x-foo"), Some("bar"));——注意,header()返回Option<&str>。 - 违规情况(表示“我拒绝了此请求”的规范信号):
response.violation()返回Option<PolicyViolation>。将templates/violation_assertion.rs放到src/tests/violation.rs,以使用可复用的assert_violation_generated()辅助函数。
INIT_SLEEP 注意事项。 许多策略会在挂载过滤器前,于 configure 中等待 1 毫秒计时器(OAuth2 introspection 策略就是一个例子)。如果在 with_entrypoint(...) 之后不调用 tester.sleep(Duration::from_millis(10)),第一个请求就会与过滤器设置产生竞态,导致不确定性失败——本地通过,却在 CI 中失败。在 src/tests/common.rs 中将 INIT_SLEEP = Duration::from_millis(10) 硬编码,并在每次 with_entrypoint(...) 之后调用 tester.sleep(INIT_SLEEP)。随附模板已包含此操作。
模式索引
| 模式 | 模板 | 摘要 |
|---|---|---|
| 最小测试 | templates/hello_test.rs | UnitTestBuilder + GET + 断言状态码为 200。编译测试目标。 |
| 可复用配置与测试器接线 | templates/test_config_helper.rs | TestConfig::base() + tester(config) 助手。放入 src/tests/common.rs。 |
| 出站上游模拟 | templates/upstream_mock.rs | with_http_upstream_from_authority + 闭包后端 + INIT_SLEEP。 |
| 捕获已发送的请求 | templates/trace_backend_capture.rs | Rc<TraceBackend> + .next(),用于断言策略发送的内容。 |
| 违规拒绝 | templates/violation_assertion.rs | response.violation() + assert_violation_generated() 助手(标记为 #[ignore],直到策略产生违规)。 |
src/tests/ 模块接线 | templates/tests_module_wiring.rs | src/tests/mod.rs 的结构,以及 src/lib.rs 中的 #[cfg(test)] mod tests; 行。 |
运行测试
cargo test --lib # unit tests only — fast inner loop (recommended)
cargo test --lib <name> -- --nocapture # single unit test with println! / log output
make test # full path: build (WASM) + ALL tests (unit AND integration)
cargo test # ALL tests (unit AND integration)
重要:脚手架生成的 Makefile 目标 make test 会运行 cargo test,同时执行 src/tests/ 中的单元测试和 tests/requests.rs 中的集成测试(使用 Docker Compose 的 #[pdk_test] 测试)。集成测试需要 Docker,且可能因与单元测试无关的环境原因而失败。迭代单元测试时,请使用 cargo test --lib 完全跳过集成测试。CI 应运行 make test,以同时检查这两层测试。
故障排查
测试在本地通过,却在 CI 中随机失败
原因:第一个请求与过滤器设置过程发生竞态。策略的 configure 函数会等待一次计时器触发,然后才启动过滤器;如果没有 INIT_SLEEP,第一次 tester.request(...) 会在过滤器挂载前运行。
修复:在 with_entrypoint(crate::configure) 之后调用 tester.sleep(Duration::from_millis(10))。始终使用 templates/test_config_helper.rs 中的 INIT_SLEEP 常量。
"no upstream registered for authority X"
原因:传递给 with_http_upstream_from_authority 的 authority 字符串,与策略在运行时将 &config.service 解析得到的字符串不匹配。常见不匹配情况:缺少端口、主机错误(配置中是 https://users-api,但策略会去掉协议)。
修复:在策略内部打印一次解析后的 authority(info!("calling {:?}", &config.service);),运行任意测试,然后复制完全一致的字符串。在 src/tests/common.rs 中将其设为常量 const UPSTREAM_AUTHORITY: &str = "...";。
with_entrypoint(crate::configure) 无法编译
原因:策略的 #[entrypoint] 函数不叫 configure。不同脚手架(以及一些手写策略)会使用 setup 或 on_configure。
修复:查看 src/lib.rs,找到 #[entrypoint] 属性,并将对应的函数路径传给 with_entrypoint。这个函数就是加载策略时启动器调用的函数。
测试中的计时器驱动行为未触发
原因:调用 tester.request(...) 不会推进模拟时间。周期性触发逻辑的策略,不会在请求期间触发计时,除非你显式推进时钟。
修复:tester.sleep(Duration::from_secs(N)) 会推进模拟时间,并触发该时间间隔内本应发生的每次计时。对于依赖时间推进的行为(限流窗口过期、缓存 TTL、计划任务),请在请求之间使用此方法。
何时升级到 pdk-test
有些行为无法通过 pdk-unit 进行真实可靠的覆盖:TLS 终止、真实监听器接线、mTLS、真实连接器链,以及策略 WASM 与 Omni 运行时之间的版本偏差。对此,脚手架提供了 tests/requests.rs,使用 #[pdk_test] + TestComposite(通过 Docker Compose 运行真实的 Omni 容器)。这就是集成测试框架。本指南不负责该工作流——这里只说明 pdk-test 的存在,以及它位于 tests/ 中,与本文所写的 src/tests/ 相互独立。
完整的 pdk-unit API 参考
完整的 pdk-unit API 范围——包括每个 UnitTestBuilder 方法、请求/响应构建器、所有后端变体、使用 #[protobuf_grpc_backend] 进行 gRPC 模拟、LDAP 凭据模拟、契约 / SLA 测试、停止迭代模式,以及 dw2pel DataWeave 辅助工具——位于 pdk-templates/templates/unit_testing.md(同级技能,共 279 行)。需要本技能未介绍的方法时,请阅读该文件。不要重新推导——该文件是权威快照。
其他资源
pdk-templates/templates/unit_testing.md— 完整的pdk-unitAPI 参考(同级技能)。develop-pdk-policy— 脚手架 / 构建 / Playground / 发布 / 发行生命周期(同级技能)。- PDK 概述:https://docs.mulesoft.com/pdk/latest/