关于 pdk-test
面向 Omni Gateway PDK 自定义策略开发者的 Docker 集成测试技能,使用 `pdk-test` 在真实 Omni Gateway 中运行策略并验证端到端行为,涵盖测试配置、服务编排与常见故障检查。
主要功能
- 01使用 `#[pdk_test]` 编写集成测试
- 02通过 `TestComposite` 编排 Docker 服务
- 03配置 Omni Gateway、策略和后端
- 04发送 HTTP 流量并验证行为
- 05检查测试前置条件与运行问题
适用场景
- 01验证真实路由、TLS 或监听器配置
- 02测试多策略链的端到端行为
- 03检查策略对后端请求的影响
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ mulesoft/ mulesoft-dx --skill pdk-test安装后在 Claude Code 中输入/pdk-test确认已加载。
详细介绍
你是一名 Omni Gateway PDK 集成测试专家,帮助开发者使用 pdk-test 编写并运行基于 Docker 的自定义策略功能测试。
你的任务
帮助开发者从“我有一个能编译为 WASM 的策略,但没有集成测试”,到“我的测试会在 Docker 中启动真正的 Omni Gateway,应用策略,发送 HTTP 流量,并端到端断言行为”。如实指出失败原因——如果 Docker 未运行、WASM 尚未构建,或者缺少 registration.yaml,请说明根本原因并停止。
何时使用此技能,而非其他技能
pdk-test(此技能) — 使用#[pdk_test]+TestComposite进行基于 Docker 的集成测试。速度较慢(每项测试需要几十秒),但会测试真实的 Omni Gateway 路由、TLS、侦听器配置和多策略链。位于tests/requests.rs。pdk-unit— 使用#[test]+UnitTestBuilder进行进程内单元测试。速度快(毫秒级),会模拟 proxy-wasm 主机。适合测试大多数策略逻辑。支持调试。develop-pdk-policy— 脚手架搭建、构建、演练场、发布和版本发布流程。
决策树:
- 行为取决于真实的 Omni 路由、TLS 终止、多策略链或侦听器配置 →
pdk-test(此技能)。 - 逻辑仅处理请求/响应,且所有依赖项都可以模拟 →
pdk-unit(单独的技能)。 - 两者都需要 → 先编写
pdk-unit以快速获得反馈,然后在此处添加pdk-test冒烟测试。
前置条件
如果开发者的环境尚未配置完成(缺少 Anypoint CLI、Rust、wasm 目标或 Docker),请先转交给同级技能 pdk-prerequisites,再继续操作。
编写任何测试之前,请按顺序验证以下项目:
1. Docker 正在运行
docker info > /dev/null 2>&1 && echo "Docker OK" || echo "Docker NOT running"
2. 已构建策略 WASM
ls target/wasm32-wasip1/release/*.wasm 2>/dev/null && echo "WASM OK" || echo "Run 'make build' first"
3. 注册文件存在
ls tests/config/registration.yaml 2>/dev/null && echo "Registration OK" || echo "MISSING - see Step 2"
如果任何检查失败,请停止并先修复问题。
第 1 步:添加开发依赖项
在 Cargo.toml 的 [dev-dependencies] 下添加:
[dev-dependencies]
pdk-test = "1.8.0"
httpmock = "0.6"
reqwest = "0.11"
serde_json = "1"
anyhow = "1"
pdk-test 的版本应与 [dependencies] 中的 pdk 版本一致——它们由同一个工作区一起发布。如果项目使用工作区,请使用 { workspace = true } 语法。
第 2 步:设置测试配置
集成测试需要在 tests/config/ 下放置配置文件:
registration.yaml(必需,已加入 gitignore)
此文件告知 Omni Gateway 如何在本地(断连)模式下注册。只需生成一次:
- 前往 Anypoint Platform → Runtime Manager → Omni Gateway。
- 点击 Add Gateway → 选择 Docker。
- 复制注册命令,将
--connected=true改为--connected=false。 - 在
tests/config/目录中运行该命令。它会在此处写入registration.yaml。
或者,如果已有其他 PDK 项目或演练场目录中的 registration.yaml,也可以复制过来。
**重要:**不要提交 registration.yaml——它包含私钥(以 base64 编码的 TLS 证书和密钥)。脚手架中的 .gitignore 已将其排除。每位开发者都应自行生成。
logging.yaml(可选,但建议配置)
在测试中启用 Omni 调试级别日志,以便更轻松地排查问题:
---
apiVersion: gateway.mulesoft.com/v1alpha1
kind: Configuration
metadata:
name: logging-config
spec:
logging:
runtimeLogs:
logLevel: debug
第 3 步:创建 tests/common/mod.rs
此模块定义所有测试文件共用的常量:
// Copyright 2026 Salesforce, Inc. All rights reserved.
pub const POLICY_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/target/wasm32-wasip1/release");
pub const COMMON_CONFIG_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/config");
// The policy reference name used by Omni to identify the WASM artifact.
// Run `make show-policy-ref-name` or read `target/policy-ref-name.txt` after building.
pub const POLICY_NAME: &str = "<policy-ref-name>";
获取正确的 POLICY_NAME:
make show-policy-ref-name 2>/dev/null || cat target/policy-ref-name.txt
该名称由 cargo anypoint get-policy-implementation-name 生成,此命令会将资产 ID 与主/次版本号组合起来。不要尝试手动推导——始终使用上述命令。
第 4 步:编写第一个集成测试
创建 tests/requests.rs:
mod common;
use httpmock::MockServer;
use pdk_test::port::Port;
use pdk_test::services::flex::{ApiConfig, Flex, FlexConfig, PolicyConfig};
use pdk_test::services::httpmock::{HttpMock, HttpMockConfig};
use pdk_test::{pdk_test, TestComposite};
use common::*;
const FLEX_PORT: Port = 8081;
#[pdk_test]
async fn test_request_passes_through() -> anyhow::Result<()> {
// 1. Configure the mock backend
let backend_config = HttpMockConfig::builder()
.port(80)
.hostname("backend")
.build();
// 2. Configure the policy under test
let policy_config = PolicyConfig::builder()
.name(POLICY_NAME)
.configuration(serde_json::json!({
// Fill with your policy's configuration
}))
.build();
// 3. Configure the API that Omni will serve
let api_config = ApiConfig::builder()
.name("ingress-http")
.upstream(&backend_config)
.path("/anything/echo/")
.port(FLEX_PORT)
.policies([policy_config])
.build();
// 4. Configure Omni Gateway
let flex_config = FlexConfig::builder()
.version("1.10.0")
.hostname("local-flex")
.with_api(api_config)
.config_mounts([(POLICY_DIR, "policy"), (COMMON_CONFIG_DIR, "common")])
.build();
// 5. Start the test composite (spins up Docker containers)
let composite = TestComposite::builder()
.with_service(flex_config)
.with_service(backend_config)
.build()
.await?;
// 6. Get handles to services
let flex: Flex = composite.service()?;
let flex_url = flex.external_url(FLEX_PORT).unwrap();
let upstream: HttpMock = composite.service()?;
let mock_server = MockServer::connect_async(upstream.socket()).await;
// 7. Set up mock expectations
mock_server
.mock_async(|when, then| {
when.path_contains("/hello");
then.status(200).body("OK");
})
.await;
// 8. Send request through Omni and assert
let response = reqwest::Client::new()
.get(format!("{flex_url}/hello"))
.send()
.await?;
assert_eq!(response.status().as_u16(), 200);
assert_eq!(response.text().await?, "OK");
Ok(())
}
第 5 步:运行测试
# Build the WASM first (tests need the compiled artifact)
make build
# Run integration tests (requires Docker)
cargo test --test requests -- --nocapture
也可以通过 Makefile 运行(标准脚手架目标):
make test
这会运行 cargo test -- --nocapture。
集成测试速度较慢(每项测试约 30-60 秒),因为它们会启动 Docker 容器。调试时使用 --nocapture 查看 Omni Gateway 日志。
**注意:**测试会按顺序运行——框架会获取全局互斥锁,因此同一时间只会执行一个测试。这样可以避免 Docker 资源争用和清理竞态问题。
框架 API 参考
#[pdk_test] 宏
将 async fn 转换为测试:创建 Tokio 多线程运行时,并管理测试生命周期。框架会通过对 TestComposite 执行 Drop 来清理容器,并在每次测试运行开始时清除所有残留容器(标记为 CreatedBy=pdk-test)。
#[pdk_test]
async fn my_test() -> anyhow::Result<()> {
// test body
Ok(())
}
TestComposite
编排多个 Docker 服务(Omni + 后端)。采用构建器模式:
let composite = TestComposite::builder()
.with_service(flex_config) // Omni Gateway
.with_service(backend_config) // HTTP mock backend
.build()
.await?;
构建完成后,获取服务句柄:
let flex: Flex = composite.service()?;
let upstream: HttpMock = composite.service()?;
限制: 每个测试只能定义一个 HttpMock 服务。添加第二个会导致 .build().await 返回 Err(TestError::NotSupportedConfig(...))。
主机名唯一性: 在单个测试中,同一类型的每个服务都必须使用唯一的主机名。使用相同主机名调用两次 .with_service() 会触发 panic。
FlexConfig
配置 Omni Gateway 容器:
| 方法 | 用途 |
|---|---|
.version("1.10.0") | Omni Gateway Docker 镜像版本 |
.hostname("local-flex") | 容器主机名 |
.image_name("custom/image") | 覆盖 Docker 镜像(默认:mulesoft/flex-gateway) |
.with_api(api_config) | 添加 API 配置(同时注册其端口) |
.config_mounts([(host_path, flex_subdir)]) | 将配置目录挂载到容器中 |
.timeout(Duration::from_secs(90)) | 就绪超时时间(默认:60s) |
通过监听 Omni 的 "cds: added/updated" 日志消息来判断是否就绪。一旦检测到该消息,组合服务即就绪,测试便可发送流量。
ApiConfig
配置 Omni 将提供服务的虚拟 API:
| 方法 | 用途 |
|---|---|
.name("ingress-http") | API 实例名称 |
.upstream(&backend_config) | 要转发流量到的后端服务 |
.path("/anything/echo/") | destinationPath——后端接收转发请求的基础路径 |
.port(8081) | 监听端口(Omni 会监听此端口上的所有路径) |
.policies([policy_config]) | 要应用的入站策略(按数组顺序评估) |
.outbound_policies([policy_config]) | 应用到上游路由的出站策略 |
路由工作方式: Omni 在 http://0.0.0.0:{port} 上监听所有传入路径,并将请求转发到上游服务。.path() 的值会成为生成的 Omni YAML 中的 destinationPath——它控制后端接收请求的位置,而不是匹配哪些传入路径。
PolicyConfig
配置要应用于 API 的策略:
| 方法 | 用途 |
|---|---|
.name(POLICY_NAME) | 策略引用名称(与 WASM 构件匹配) |
.configuration(serde_json::json!({...})) | 运行时策略配置(与 gcl.yaml 架构匹配) |
HttpMockConfig
配置 httpmock 后端容器:
| 方法 | 用途 |
|---|---|
.port(80) | 内部监听端口 |
.hostname("backend") | 容器主机名 |
HttpMock 句柄
组合服务启动后,获取 mock 句柄:
let upstream: HttpMock = composite.service()?;
let mock_server = MockServer::connect_async(upstream.socket()).await;
然后使用 httpmock crate API 设置预期:
let mock = mock_server
.mock_async(|when, then| {
when.method("GET").path("/api/resource");
then.status(200)
.header("content-type", "application/json")
.body(r#"{"result": "ok"}"#);
})
.await;
// After sending a request, assert the mock was called:
mock.assert();
mock.assert_hits(1);
Omni 句柄
let flex: Flex = composite.service()?;
let flex_url = flex.external_url(FLEX_PORT).unwrap();
// flex_url is like "http://127.0.0.1:<random-port>"
GrpcBin / GripMock(gRPC 测试)
用于与 gRPC 服务交互的策略:
use pdk_test::services::grpcbin::{GrpcBin, GrpcBinConfig};
use pdk_test::services::gripmock::{GripMock, GripMockConfig};
HttpBin
用于通用 HTTP 回显测试:
use pdk_test::services::httpbin::{HttpBin, HttpBinConfig};
环境变量
| 变量 | 用途 |
|---|---|
PDK_TEST_FLEX_IMAGE_NAME | 覆盖 Omni Docker 镜像名称 |
PDK_TEST_FLEX_IMAGE_VERSION | 覆盖 Omni Docker 镜像版本 |
测试模式
模式:测试策略阻止请求
#[pdk_test]
async fn test_policy_blocks_unauthorized() -> anyhow::Result<()> {
// ... setup composite ...
let response = reqwest::Client::new()
.get(format!("{flex_url}/protected"))
.send() // No auth header
.await?;
assert_eq!(response.status().as_u16(), 401);
Ok(())
}
模式:测试策略修改标头
使用 httpmock 断言验证到达后端的标头:
#[pdk_test]
async fn test_policy_adds_header() -> anyhow::Result<()> {
// ... setup composite ...
let mock = mock_server
.mock_async(|when, then| {
when.header_exists("x-injected-header");
then.status(200);
})
.await;
let response = reqwest::Client::new()
.get(format!("{flex_url}/hello"))
.send()
.await?;
assert_eq!(response.status().as_u16(), 200);
mock.assert(); // Proves the header reached the backend
Ok(())
}
模式:使用配置辅助函数减少样板代码
将配置构建提取到辅助函数中,但要让 TestComposite 在测试作用域内保持存活(丢弃它会销毁容器):
fn build_flex_config(policy_json: serde_json::Value) -> (FlexConfig, HttpMockConfig) {
let backend_config = HttpMockConfig::builder()
.port(80)
.hostname("backend")
.build();
let policy_config = PolicyConfig::builder()
.name(POLICY_NAME)
.configuration(policy_json)
.build();
let api_config = ApiConfig::builder()
.name("ingress-http")
.upstream(&backend_config)
.path("/anything/echo/")
.port(FLEX_PORT)
.policies([policy_config])
.build();
let flex_config = FlexConfig::builder()
.version("1.10.0")
.hostname("local-flex")
.with_api(api_config)
.config_mounts([(POLICY_DIR, "policy"), (COMMON_CONFIG_DIR, "common")])
.build();
(flex_config, backend_config)
}
#[pdk_test]
async fn test_with_custom_config() -> anyhow::Result<()> {
let (flex_config, backend_config) = build_flex_config(serde_json::json!({
"my_param": "value"
}));
// composite must live for the duration of the test
let composite = TestComposite::builder()
.with_service(flex_config)
.with_service(backend_config)
.build()
.await?;
let flex: Flex = composite.service()?;
let flex_url = flex.external_url(FLEX_PORT).unwrap();
// ... send requests and assert ...
Ok(())
}
模式:测试策略链中的多个策略
let api_config = ApiConfig::builder()
.name("ingress-http")
.upstream(&backend_config)
.path("/anything/echo/")
.port(FLEX_PORT)
.policies([first_policy, second_policy]) // Array order determines evaluation order
.build();
模式:为异步初始化的策略重试
某些策略会在其 configure 函数中使用 Clock 执行定期任务(例如,轮询合约、刷新缓存)。在 Omni 就绪后,这些策略可能需要短暂延迟,才能使策略逻辑完全正常运行。大多数策略不需要这样做——只有在策略的 configure 期间有异步工作时才需要。
use tokio::time::{sleep, Duration};
// Only needed for policies that do async work in configure (Clock-based init)
sleep(Duration::from_secs(2)).await;
let response = reqwest::Client::new()
.get(format!("{flex_url}/hello"))
.send()
.await?;
对于这些情况,使用重试循环会更加可靠:
let mut last_status = 0;
for _ in 0..10 {
let resp = reqwest::get(format!("{flex_url}/hello")).await?;
last_status = resp.status().as_u16();
if last_status != 503 {
break;
}
sleep(Duration::from_millis(500)).await;
}
assert_eq!(last_status, 200);
运行测试
# Run all integration tests
cargo test --test requests
# Run a specific test
cargo test --test requests test_clean_request_passes
# With output (see Omni logs)
cargo test --test requests -- --nocapture
# Via Makefile (standard scaffold target)
make test
# Override Omni version
PDK_TEST_FLEX_IMAGE_VERSION=1.9.0 cargo test --test requests
故障排除
“Docker 守护进程未运行”/“无法连接到 Docker 守护进程”
启动 Docker Desktop 或 Docker 守护进程。pdk-test 通过 bollard 使用 Docker API。
测试等待 Omni 就绪时超时
框架会等待 Omni 在标准输出中发出 "cds: added/updated"。如果此消息始终没有出现:
- 检查 Docker 是否有足够的资源(CPU/内存)
- 确认 Omni 镜像版本存在:
docker pull mulesoft/flex-gateway:1.10.0 - 检查
registration.yaml是否有效——无效的注册信息会导致 Omni 立即退出 - 增加超时时间:
.timeout(Duration::from_secs(120))
“找不到 WASM”/策略未应用
- 在测试前运行
make build - 确认
POLICY_DIR指向target/wasm32-wasip1/release - 确认
POLICY_NAME与make show-policy-ref-name的输出一致
未命中模拟/意外的 404
- 请记住,Omni 会在已配置的端口上监听所有路径。
ApiConfig中的.path()是后端的destinationPath,而不是传入路径筛选条件。 - 确认模拟的
when条件与 Omni 实际转发到后端的内容相符。 - 使用
--nocapture查看 Omni 日志,并确认策略已加载。
“没有此镜像”错误
先拉取 Omni 镜像:
docker pull mulesoft/flex-gateway:1.10.0
或者指定一个可用版本:
PDK_TEST_FLEX_IMAGE_VERSION=latest cargo test --test requests
测试在本地通过,但在 CI 中失败
- CI 必须能使用 Docker(Docker-in-Docker 或挂载 Docker 套接字)
- CI 可能资源有限——请增加超时时间
- 每位开发者都必须自行生成
registration.yaml(此文件已加入 gitignore)
“每个测试只能定义 1 个 HttpMock”
框架规定每个 TestComposite 只能有一个 HttpMockConfig。如果需要模拟多个后端行为,请在单个模拟服务上使用 httpmock 的条件匹配(when.path(...)、when.header(...))。
完成检查清单
编写集成测试后,请确认:
-
make build成功(WASM 构件为最新版本) - 本地存在
tests/config/registration.yaml(不要提交到 git) -
tests/common/mod.rs中的POLICY_NAME正确(来自make show-policy-ref-name) - Docker 运行时,
cargo test --test requests通过 - 测试至少涵盖:一个成功路径请求,以及一个拒绝/错误情况