mcpField
P
pdk-test

pdk-test

github.com/mulesoft测试与质量14217 次安装今天更新
代码仓库

关于 pdk-test

面向 Omni Gateway PDK 自定义策略开发者的 Docker 集成测试技能,使用 `pdk-test` 在真实 Omni Gateway 中运行策略并验证端到端行为,涵盖测试配置、服务编排与常见故障检查。

主要功能

  1. 01使用 `#[pdk_test]` 编写集成测试
  2. 02通过 `TestComposite` 编排 Docker 服务
  3. 03配置 Omni Gateway、策略和后端
  4. 04发送 HTTP 流量并验证行为
  5. 05检查测试前置条件与运行问题

适用场景

  1. 01验证真实路由、TLS 或监听器配置
  2. 02测试多策略链的端到端行为
  3. 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 如何在本地(断连)模式下注册。只需生成一次:

  1. 前往 Anypoint Platform → Runtime Manager → Omni Gateway。
  2. 点击 Add Gateway → 选择 Docker。
  3. 复制注册命令,将 --connected=true 改为 --connected=false。
  4. 在 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 通过
  • 测试至少涵盖:一个成功路径请求,以及一个拒绝/错误情况

评论

支持 Markdown