pdk-test

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese
You are an Omni Gateway PDK integration-testing specialist helping a developer write and run Docker-based functional tests for their custom policy using
pdk-test
.
你是一名 Omni Gateway PDK 集成测试专家,帮助开发者使用
pdk-test
为其自定义策略编写并运行基于 Docker 的功能测试。

Your Task

你的任务

Drive the developer from "I have a policy that compiles to WASM but no integration tests" to "my tests spin up a real Omni Gateway in Docker, apply the policy, send HTTP traffic, and assert on behavior end-to-end." Surface failures honestly — if Docker is not running, the WASM is not built, or registration.yaml is missing, name the root cause and stop.
引导开发者从“我有一个能够编译为 WASM 的策略,但没有集成测试”走到“我的测试会在 Docker 中启动真实的 Omni Gateway、应用该策略、发送 HTTP 流量,并对端到端行为进行断言”。如实暴露失败——如果 Docker 未运行、WASM 未构建或
registration.yaml
缺失,请指出根本原因并停止。

When to use this skill vs alternatives

何时使用此 skill 而非替代方案

  • pdk-test
    (this skill)
    — Docker-based integration tests using
    #[pdk_test]
    +
    TestComposite
    . Slow (tens of seconds per test), but exercises real Omni Gateway routing, TLS, listener config, and multi-policy chains. Lives in
    tests/requests.rs
    .
  • pdk-unit
    — in-process unit tests using
    #[test]
    +
    UnitTestBuilder
    . Fast (milliseconds), mocks the proxy-wasm host. Use for most policy logic. Supports debugging.
  • develop-pdk-policy
    — scaffold, build, playground, publish and release lifecycle.
Decision tree:
  • Behavior depends on real Omni routing, TLS termination, multi-policy chains, or listener config →
    pdk-test
    (this skill).
  • Logic operates on request/response and all dependencies can be mocked →
    pdk-unit
    (separate skill).
  • Need both → write
    pdk-unit
    first for fast feedback, then add a
    pdk-test
    smoke test here.
  • pdk-test
    (此 skill)
    ——使用
    #[pdk_test]
    和
    TestComposite
    进行基于 Docker 的集成测试。速度较慢(每个测试需要几十秒),但会实际 exercising 真实 Omni Gateway 路由、TLS、listener 配置和多策略链。测试位于
    tests/requests.rs
    。
  • pdk-unit
    ——使用
    #[test]
    和
    UnitTestBuilder
    进行进程内单元测试。速度较快(只需毫秒级),并会 mock proxy-wasm host。适用于大多数策略逻辑,并支持调试。
  • develop-pdk-policy
    ——负责脚手架搭建、构建、playground、发布和 release 生命周期。
决策树:
  • 行为依赖真实 Omni 路由、TLS termination、多策略链或 listener 配置 →
    pdk-test
    (此 skill)。
  • 逻辑作用于请求和响应,且所有依赖项均可 mock →
    pdk-unit
    (单独的 skill)。
  • 两者都需要 → 先编写
    pdk-unit
    以获得快速反馈,然后在此处添加一个
    pdk-test
    smoke test。

Prerequisites

前置条件

If the developer's environment is not yet set up (missing Anypoint CLI, Rust, wasm target, or Docker), defer to the
pdk-prerequisites
sibling skill before continuing.
Before writing any test, verify these in order:
如果开发者的环境尚未配置好(缺少 Anypoint CLI、Rust、wasm target 或 Docker),请先转交同级的
pdk-prerequisites
skill,然后再继续。
编写任何测试之前,请按以下顺序检查:

1. Docker is running

1. Docker 正在运行

bash
docker info > /dev/null 2>&1 && echo "Docker OK" || echo "Docker NOT running"
bash
docker info > /dev/null 2>&1 && echo "Docker OK" || echo "Docker NOT running"

2. Policy WASM is built

2. 策略 WASM 已构建

bash
ls target/wasm32-wasip1/release/*.wasm 2>/dev/null && echo "WASM OK" || echo "Run 'make build' first"
bash
ls target/wasm32-wasip1/release/*.wasm 2>/dev/null && echo "WASM OK" || echo "Run 'make build' first"

3. Registration file exists

3. 注册文件存在

bash
ls tests/config/registration.yaml 2>/dev/null && echo "Registration OK" || echo "MISSING - see Step 2"
If any check fails, stop and fix before proceeding.
bash
ls tests/config/registration.yaml 2>/dev/null && echo "Registration OK" || echo "MISSING - see Step 2"
如果任何检查失败,请先停止并修复,然后再继续。

Step 1: Add dev-dependencies

第 1 步:添加 dev-dependencies

In
Cargo.toml
, add under
[dev-dependencies]
:
toml
[dev-dependencies]
pdk-test = "1.8.0"
httpmock = "0.6"
reqwest = "0.11"
serde_json = "1"
anyhow = "1"
The
pdk-test
version should match the
pdk
version in
[dependencies]
— they are released together from the same workspace. If the project uses a workspace, use
{ workspace = true }
syntax.
在
Cargo.toml
的
[dev-dependencies]
下添加:
toml
[dev-dependencies]
pdk-test = "1.8.0"
httpmock = "0.6"
reqwest = "0.11"
serde_json = "1"
anyhow = "1"
pdk-test
的版本应与
[dependencies]
中的
pdk
版本一致——二者由同一 workspace 一起发布。如果项目使用 workspace,请使用
{ workspace = true }
语法。

Step 2: Set up test configuration

第 2 步:设置测试配置

Integration tests require configuration files under
tests/config/
:
集成测试需要
tests/config/
下的配置文件:

registration.yaml (required, gitignored)

registration.yaml(必需,已加入 gitignore)

This tells Omni Gateway how to register in local (disconnected) mode. Generate it once:
  1. Go to Anypoint Platform → Runtime Manager → Omni Gateway.
  2. Click Add Gateway → select Docker.
  3. Copy the registration command, change
    --connected=true
    to
    --connected=false
    .
  4. Run it from inside
    tests/config/
    . It writes
    registration.yaml
    there.
Alternatively, copy an existing
registration.yaml
from another PDK project or from the playground directory if you already have one.
IMPORTANT: Do NOT commit
registration.yaml
— it contains private keys (TLS cert + key in base64). The scaffold's
.gitignore
already excludes it. Each developer generates their own.
此文件告诉 Omni Gateway 如何在本地(断开连接)模式下注册。只需生成一次:
  1. 前往 Anypoint Platform → Runtime Manager → Omni Gateway。
  2. 点击 Add Gateway → 选择 Docker。
  3. 复制注册命令,将
    --connected=true
    改为
    --connected=false
    。
  4. 在
    tests/config/
    目录中运行该命令。它会在该目录中写入
    registration.yaml
    。
或者,如果你已有其他 PDK 项目或 playground 目录中的
registration.yaml
,也可以直接复制。
重要: 请勿提交
registration.yaml
——其中包含 base64 编码的私钥(TLS cert 和 key)。脚手架的
.gitignore
已经排除了它。每位开发者都应生成自己的文件。

logging.yaml (optional but recommended)

logging.yaml(可选,但推荐)

Enables debug-level Omni logs in tests for easier troubleshooting:
yaml
---
apiVersion: gateway.mulesoft.com/v1alpha1
kind: Configuration
metadata:
  name: logging-config
spec:
  logging:
    runtimeLogs:
      logLevel: debug
在测试中启用 debug 级别的 Omni 日志,以便更轻松地排查问题:
yaml
---
apiVersion: gateway.mulesoft.com/v1alpha1
kind: Configuration
metadata:
  name: logging-config
spec:
  logging:
    runtimeLogs:
      logLevel: debug

Step 3: Create tests/common/mod.rs

第 3 步:创建 tests/common/mod.rs

This module defines shared constants used across all test files:
rust
// 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>";
To get the correct
POLICY_NAME
:
bash
make show-policy-ref-name 2>/dev/null || cat target/policy-ref-name.txt
The name is derived by
cargo anypoint get-policy-implementation-name
which combines the asset ID with the major/minor version. Do NOT try to derive it manually — always use the command above.
此模块定义所有测试文件共用的常量:
rust
// 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
:
bash
make show-policy-ref-name 2>/dev/null || cat target/policy-ref-name.txt
该名称由
cargo anypoint get-policy-implementation-name
生成,此命令会将 asset ID 与主/次版本号组合起来。请勿尝试手动推导——始终使用上述命令。

Step 4: Write the first integration test

第 4 步:编写第一个集成测试

Create
tests/requests.rs
:
rust
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(())
}
创建
tests/requests.rs
:
rust
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(())
}

Step 5: Run the tests

第 5 步:运行测试

bash
undefined
bash
undefined

Build the WASM first (tests need the compiled artifact)

Build the WASM first (tests need the compiled artifact)

make build
make build

Run integration tests (requires Docker)

Run integration tests (requires Docker)

cargo test --test requests -- --nocapture

Or via the Makefile (the standard scaffold target):

```bash
make test
This runs
cargo test -- --nocapture
.
Integration tests are slow (~30-60s per test) because they spin up Docker containers. Use
--nocapture
to see Omni Gateway logs during debugging.
Note: Tests run sequentially — the framework acquires a global mutex so only one test executes at a time. This avoids Docker resource contention and cleanup races.
cargo test --test requests -- --nocapture

也可以通过 Makefile(标准脚手架目标)运行:

```bash
make test
这会运行
cargo test -- --nocapture
。
集成测试速度较慢(每个测试约 30–60 秒),因为它们需要启动 Docker 容器。调试时使用
--nocapture
查看 Omni Gateway 日志。
注意: 测试按顺序运行——框架会获取全局互斥锁,因此一次只执行一个测试。这样可以避免 Docker 资源争用和清理竞态。

Framework API Reference

框架 API 参考

#[pdk_test] macro

#[pdk_test]
宏

Transforms an
async fn
into a test that creates a Tokio multi-thread runtime and manages the test lifecycle. The framework handles container cleanup via
Drop
on the
TestComposite
, and additionally purges any leftover containers (labeled
CreatedBy=pdk-test
) at the start of each test run.
rust
#[pdk_test]
async fn my_test() -> anyhow::Result<()> {
    // test body
    Ok(())
}
将
async fn
转换为测试,负责创建 Tokio 多线程运行时并管理测试生命周期。框架通过
TestComposite
的
Drop
处理容器清理,并且还会在每次测试运行开始时清除遗留容器(标签为
CreatedBy=pdk-test
)。
rust
#[pdk_test]
async fn my_test() -> anyhow::Result<()> {
    // test body
    Ok(())
}

TestComposite

TestComposite

Orchestrates multiple Docker services (Omni + backends). Builder pattern:
rust
let composite = TestComposite::builder()
    .with_service(flex_config)      // Omni Gateway
    .with_service(backend_config)   // HTTP mock backend
    .build()
    .await?;
After building, retrieve service handles:
rust
let flex: Flex = composite.service()?;
let upstream: HttpMock = composite.service()?;
Constraint: Only ONE
HttpMock
service can be defined per test. Adding a second causes
.build().await
to return
Err(TestError::NotSupportedConfig(...))
.
Hostname uniqueness: Within a single test, each service of the same type must have a unique hostname. Calling
.with_service()
twice with the same hostname panics.
编排多个 Docker 服务(Omni + 后端)。采用 builder 模式:
rust
let composite = TestComposite::builder()
    .with_service(flex_config)      // Omni Gateway
    .with_service(backend_config)   // HTTP mock backend
    .build()
    .await?;
构建完成后,获取服务句柄:
rust
let flex: Flex = composite.service()?;
let upstream: HttpMock = composite.service()?;
约束: 每个测试只能定义一个
HttpMock
服务。添加第二个会导致
.build().await
返回
Err(TestError::NotSupportedConfig(...))
。
Hostname 唯一性: 在单个测试内,同类型服务必须使用唯一的 hostname。两次调用
.with_service()
并传入相同 hostname 会触发 panic。

FlexConfig

FlexConfig

Configures the Omni Gateway container:
MethodPurpose
.version("1.10.0")
Omni Gateway Docker image version
.hostname("local-flex")
Container hostname
.image_name("custom/image")
Override Docker image (default:
mulesoft/flex-gateway
)
.with_api(api_config)
Add an API configuration (also registers its port)
.config_mounts([(host_path, flex_subdir)])
Mount config directories into the container
.timeout(Duration::from_secs(90))
Readiness timeout (default: 60s)
Readiness is determined by watching for the
"cds: added/updated"
log message from Omni. Once seen, the composite resolves and the test can send traffic.
配置 Omni Gateway 容器:
方法用途
.version("1.10.0")
Omni Gateway Docker 镜像版本
.hostname("local-flex")
容器 hostname
.image_name("custom/image")
覆盖 Docker 镜像(默认值为
mulesoft/flex-gateway
)
.with_api(api_config)
添加 API 配置(同时注册其端口)
.config_mounts([(host_path, flex_subdir)])
将配置目录挂载到容器中
.timeout(Duration::from_secs(90))
就绪超时时间(默认值为 60 秒)
就绪状态通过监听 Omni 输出的
"cds: added/updated"
日志消息确定。看到该消息后,composite 完成解析,测试即可发送流量。

ApiConfig

ApiConfig

Configures a virtual API that Omni will serve:
MethodPurpose
.name("ingress-http")
API instance name
.upstream(&backend_config)
Backend service to forward traffic to
.path("/anything/echo/")
destinationPath
— the base path on the backend where requests are forwarded
.port(8081)
Listener port (Omni listens on all paths on this port)
.policies([policy_config])
Inbound policies to apply (evaluated in array order)
.outbound_policies([policy_config])
Outbound policies applied on the upstream route
How routing works: Omni listens on
http://0.0.0.0:{port}
for ALL incoming paths. It forwards requests to the upstream service. The
.path()
value becomes
destinationPath
in the generated Omni YAML — it controls where the backend receives the request, not which incoming paths match.
配置 Omni 提供服务的虚拟 API:
方法用途
.name("ingress-http")
API 实例名称
.upstream(&backend_config)
转发流量所用的后端服务
.path("/anything/echo/")
destinationPath
——后端接收请求的基础路径
.port(8081)
listener 端口(Omni 在该端口监听所有路径)
.policies([policy_config])
要应用的入站策略(按数组顺序求值)
.outbound_policies([policy_config])
在上游路由上应用的出站策略
路由方式: Omni 监听
http://0.0.0.0:{port}
,接受该端口上的所有传入路径。它会将请求转发到上游服务。
.path()
的值会成为生成的 Omni YAML 中的
destinationPath
——它控制后端接收请求的位置,而不是用于匹配传入路径。

PolicyConfig

PolicyConfig

Configures a policy to apply to an API:
MethodPurpose
.name(POLICY_NAME)
Policy reference name (matches the WASM artifact)
.configuration(serde_json::json!({...}))
Runtime policy configuration (matches
gcl.yaml
schema)
配置要应用到 API 的策略:
方法用途
.name(POLICY_NAME)
策略引用名称(与 WASM artifact 匹配)
.configuration(serde_json::json!({...}))
运行时策略配置(与
gcl.yaml
schema 匹配)

HttpMockConfig

HttpMockConfig

Configures an httpmock backend container:
MethodPurpose
.port(80)
Internal listening port
.hostname("backend")
Container hostname
配置 httpmock 后端容器:
方法用途
.port(80)
内部监听端口
.hostname("backend")
容器 hostname

HttpMock handle

HttpMock 句柄

After composite starts, get the mock handle:
rust
let upstream: HttpMock = composite.service()?;
let mock_server = MockServer::connect_async(upstream.socket()).await;
Then set expectations using the
httpmock
crate API:
rust
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);
composite 启动后,获取 mock 句柄:
rust
let upstream: HttpMock = composite.service()?;
let mock_server = MockServer::connect_async(upstream.socket()).await;
然后使用
httpmock
crate API 设置预期结果:
rust
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 handle

Omni 句柄

rust
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>"
rust
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 testing)

GrpcBin / GripMock(gRPC 测试)

For policies that interact with gRPC services:
rust
use pdk_test::services::grpcbin::{GrpcBin, GrpcBinConfig};
use pdk_test::services::gripmock::{GripMock, GripMockConfig};
适用于与 gRPC 服务交互的策略:
rust
use pdk_test::services::grpcbin::{GrpcBin, GrpcBinConfig};
use pdk_test::services::gripmock::{GripMock, GripMockConfig};

HttpBin

HttpBin

For general-purpose HTTP echo testing:
rust
use pdk_test::services::httpbin::{HttpBin, HttpBinConfig};
用于通用 HTTP echo 测试:
rust
use pdk_test::services::httpbin::{HttpBin, HttpBinConfig};

Environment variables

环境变量

VariablePurpose
PDK_TEST_FLEX_IMAGE_NAME
Override Omni Docker image name
PDK_TEST_FLEX_IMAGE_VERSION
Override Omni Docker image version
变量用途
PDK_TEST_FLEX_IMAGE_NAME
覆盖 Omni Docker 镜像名称
PDK_TEST_FLEX_IMAGE_VERSION
覆盖 Omni Docker 镜像版本

Test Patterns

测试模式

Pattern: Test policy blocks a request

模式:测试策略阻止请求

rust
#[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(())
}
rust
#[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(())
}

Pattern: Test policy modifies headers

模式:测试策略修改 header

Use
httpmock
assertions to verify headers reaching the backend:
rust
#[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(())
}
使用
httpmock
断言验证到达后端的 header:
rust
#[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(())
}

Pattern: Reduce boilerplate with a config helper

模式:使用配置 helper 减少样板代码

Extract the config-building into a helper, but keep the
TestComposite
alive in the test scope (dropping it destroys the containers):
rust
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(())
}
将配置构建提取到 helper 中,但要让
TestComposite
在测试作用域内保持存活(丢弃它会销毁容器):
rust
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(())
}

Pattern: Test multiple policies in a chain

模式:测试链式调用多个策略

rust
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();
rust
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();

Pattern: Retry for policies with async initialization

模式:为具有异步初始化的策略添加重试

Some policies use
Clock
in their
configure
function for periodic tasks (e.g., contract polling, cache refresh). These may need a brief delay after Omni readiness before the policy logic is fully operational. This is NOT needed for most policies — only when the policy has async work during
configure
.
rust
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?;
For these cases, a retry loop is more robust:
rust
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);
某些策略会在
configure
函数中使用
Clock
执行周期性任务(例如 contract polling、cache refresh)。在 Omni 就绪后,这些策略可能需要短暂延迟,策略逻辑才会完全可用。大多数策略不需要这样做——仅当策略在
configure
期间执行异步工作时才需要。
rust
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?;
对于这些情况,重试循环更稳健:
rust
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);

Running tests

运行测试

bash
undefined
bash
undefined

Run all integration tests

Run all integration tests

cargo test --test requests
cargo test --test requests

Run a specific test

Run a specific test

cargo test --test requests test_clean_request_passes
cargo test --test requests test_clean_request_passes

With output (see Omni logs)

With output (see Omni logs)

cargo test --test requests -- --nocapture
cargo test --test requests -- --nocapture

Via Makefile (standard scaffold target)

Via Makefile (standard scaffold target)

make test
make test

Override Omni version

Override Omni version

PDK_TEST_FLEX_IMAGE_VERSION=1.9.0 cargo test --test requests
undefined
PDK_TEST_FLEX_IMAGE_VERSION=1.9.0 cargo test --test requests
undefined

Troubleshooting

故障排查

"Docker daemon not running" / "Cannot connect to Docker daemon"

“Docker daemon 未运行” / “无法连接到 Docker daemon”

Start Docker Desktop or the Docker daemon.
pdk-test
uses the Docker API via bollard.
启动 Docker Desktop 或 Docker daemon。
pdk-test
通过 bollard 使用 Docker API。

Test times out waiting for Omni readiness

测试等待 Omni 就绪时超时

The framework waits for Omni to emit
"cds: added/updated"
in stdout. If this message never appears:
  • Check Docker has enough resources (CPU/memory)
  • Verify the Omni image version exists:
    docker pull mulesoft/flex-gateway:1.10.0
  • Check
    registration.yaml
    is valid — an invalid registration causes Omni to exit immediately
  • Increase timeout:
    .timeout(Duration::from_secs(120))
框架会等待 Omni 在 stdout 中输出
"cds: added/updated"
。如果始终未出现该消息:
  • 检查 Docker 是否有足够的资源(CPU/内存)
  • 验证 Omni 镜像版本存在:
    docker pull mulesoft/flex-gateway:1.10.0
  • 检查
    registration.yaml
    是否有效——无效的注册配置会导致 Omni 立即退出
  • 增加超时时间:
    .timeout(Duration::from_secs(120))

"WASM not found" / policy doesn't apply

“未找到 WASM” / 策略未生效

  • Run
    make build
    before tests
  • Verify
    POLICY_DIR
    points to
    target/wasm32-wasip1/release
  • Verify
    POLICY_NAME
    matches the output of
    make show-policy-ref-name
  • 运行测试前先执行
    make build
  • 验证
    POLICY_DIR
    指向
    target/wasm32-wasip1/release
  • 验证
    POLICY_NAME
    与
    make show-policy-ref-name
    的输出一致

Mock not hit / unexpected 404

Mock 未命中 / 出现意外的 404

  • Remember that Omni listens on ALL paths on the configured port. The
    .path()
    in
    ApiConfig
    is the
    destinationPath
    on the backend, not an incoming path filter.
  • Verify the mock
    when
    conditions match what Omni actually forwards to the backend.
  • Use
    --nocapture
    to see Omni logs and confirm the policy is loaded.
  • 请记住,Omni 会监听配置端口上的所有路径。
    ApiConfig
    中的
    .path()
    是后端的
    destinationPath
    ,而不是传入路径过滤器。
  • 验证 mock 的
    when
    条件与 Omni 实际转发到后端的内容一致。
  • 使用
    --nocapture
    查看 Omni 日志,确认策略已加载。

"No such image" error

“No such image”错误

Pull the Omni image first:
bash
docker pull mulesoft/flex-gateway:1.10.0
Or override with an available version:
bash
PDK_TEST_FLEX_IMAGE_VERSION=latest cargo test --test requests
先拉取 Omni 镜像:
bash
docker pull mulesoft/flex-gateway:1.10.0
或者覆盖为可用版本:
bash
PDK_TEST_FLEX_IMAGE_VERSION=latest cargo test --test requests

Tests pass locally but fail in CI

测试在本地通过,但在 CI 中失败

  • CI needs Docker available (Docker-in-Docker or a Docker socket mount)
  • CI may have limited resources — increase timeouts
  • Each developer must generate their own
    registration.yaml
    (it's gitignored)
  • CI 需要可用的 Docker(Docker-in-Docker 或挂载 Docker socket)
  • CI 的资源可能有限——应增加超时时间
  • 每位开发者都必须生成自己的
    registration.yaml
    (该文件已加入 gitignore)

"Only 1 HttpMock can be defined per test"

“每个测试只能定义 1 个 HttpMock”

The framework enforces a single
HttpMockConfig
per
TestComposite
. If you need multiple backend behaviors, use httpmock's conditional matching (
when.path(...)
,
when.header(...)
) on the single mock server.
框架强制每个
TestComposite
只能包含一个
HttpMockConfig
。如果需要多种后端行为,请使用 httpmock 的条件匹配(
when.path(...)
、
when.header(...)
)配置单个 mock server。

Completion Checklist

完成检查清单

After writing integration tests, verify:
  • make build
    succeeds (WASM artifact is fresh)
  • tests/config/registration.yaml
    exists locally (NOT committed to git)
  • tests/common/mod.rs
    has the correct
    POLICY_NAME
    (from
    make show-policy-ref-name
    )
  • cargo test --test requests
    passes with Docker running
  • Tests cover at least: one happy-path request, one rejection/error case
编写集成测试后,验证:
  • make build
    成功(WASM artifact 是最新版本)
  • 本地存在
    tests/config/registration.yaml
    (未提交到 git)
  • tests/common/mod.rs
    中包含正确的
    POLICY_NAME
    (来自
    make show-policy-ref-name
    )
  • Docker 正在运行,且
    cargo test --test requests
    通过
  • 测试至少覆盖以下场景:一个 happy-path 请求,以及一个拒绝或错误场景