pdk-test
Write and run integration tests for custom Omni Gateway policies using the `pdk-test` framework — Docker-based, real Omni Gateway routing, `#[pdk_test]` macro, `TestComposite` orchestration, `HttpMock` / `GrpcBin` backends, `reqwest` assertions. Use whenever the user mentions "PDK integration test", "pdk-test", "functional test PDK", "#[pdk_test]", "TestComposite", "FlexConfig", "tests/requests.rs", "make test", or asks "how do I test my policy against real Omni", "how do I set up Docker-based tests for PDK", "why does my pdk-test timeout", "how do I mock a backend in integration tests".
How do I install this agent skill?
npx skills add https://github.com/mulesoft/mulesoft-dx --skill pdk-testIs this agent skill safe to install?
- Gen Agent Trust Hubpass
The skill provides legitimate instructions for writing and running integration tests for custom Flex Gateway policies using the pdk-test framework. It correctly identifies sensitive files containing private keys and provides appropriate warnings to prevent their accidental exposure in version control.
- Socketpass
No alerts
- Snykpass
Risk: LOW · No issues
What does this agent skill do?
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.
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.
When to use this skill vs alternatives
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 intests/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-unitfirst for fast feedback, then add apdk-testsmoke test here.
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:
1. Docker is running
docker info > /dev/null 2>&1 && echo "Docker OK" || echo "Docker NOT running"
2. Policy WASM is built
ls target/wasm32-wasip1/release/*.wasm 2>/dev/null && echo "WASM OK" || echo "Run 'make build' first"
3. Registration file exists
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.
Step 1: Add dev-dependencies
In Cargo.toml, add under [dev-dependencies]:
[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.
Step 2: Set up test configuration
Integration tests require configuration files under tests/config/:
registration.yaml (required, gitignored)
This tells Omni Gateway how to register in local (disconnected) mode. Generate it once:
- Go to Anypoint Platform → Runtime Manager → Omni Gateway.
- Click Add Gateway → select Docker.
- Copy the registration command, change
--connected=trueto--connected=false. - Run it from inside
tests/config/. It writesregistration.yamlthere.
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.
logging.yaml (optional but recommended)
Enables debug-level Omni logs in tests for easier troubleshooting:
---
apiVersion: gateway.mulesoft.com/v1alpha1
kind: Configuration
metadata:
name: logging-config
spec:
logging:
runtimeLogs:
logLevel: debug
Step 3: Create tests/common/mod.rs
This module defines shared constants used across all test files:
// 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:
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.
Step 4: Write the first integration test
Create 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(())
}
Step 5: Run the tests
# Build the WASM first (tests need the compiled artifact)
make build
# Run integration tests (requires Docker)
cargo test --test requests -- --nocapture
Or via the Makefile (the standard scaffold target):
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.
Framework API Reference
#[pdk_test] macro
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.
#[pdk_test]
async fn my_test() -> anyhow::Result<()> {
// test body
Ok(())
}
TestComposite
Orchestrates multiple Docker services (Omni + backends). Builder pattern:
let composite = TestComposite::builder()
.with_service(flex_config) // Omni Gateway
.with_service(backend_config) // HTTP mock backend
.build()
.await?;
After building, retrieve service handles:
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.
FlexConfig
Configures the Omni Gateway container:
| Method | Purpose |
|---|---|
.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.
ApiConfig
Configures a virtual API that Omni will serve:
| Method | Purpose |
|---|---|
.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.
PolicyConfig
Configures a policy to apply to an API:
| Method | Purpose |
|---|---|
.name(POLICY_NAME) | Policy reference name (matches the WASM artifact) |
.configuration(serde_json::json!({...})) | Runtime policy configuration (matches gcl.yaml schema) |
HttpMockConfig
Configures an httpmock backend container:
| Method | Purpose |
|---|---|
.port(80) | Internal listening port |
.hostname("backend") | Container hostname |
HttpMock handle
After composite starts, get the mock handle:
let upstream: HttpMock = composite.service()?;
let mock_server = MockServer::connect_async(upstream.socket()).await;
Then set expectations using the 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 handle
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)
For policies that interact with gRPC services:
use pdk_test::services::grpcbin::{GrpcBin, GrpcBinConfig};
use pdk_test::services::gripmock::{GripMock, GripMockConfig};
HttpBin
For general-purpose HTTP echo testing:
use pdk_test::services::httpbin::{HttpBin, HttpBinConfig};
Environment variables
| Variable | Purpose |
|---|---|
PDK_TEST_FLEX_IMAGE_NAME | Override Omni Docker image name |
PDK_TEST_FLEX_IMAGE_VERSION | Override Omni Docker image version |
Test Patterns
Pattern: Test policy blocks a request
#[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
Use httpmock assertions to verify headers reaching the backend:
#[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
Extract the config-building into a helper, but keep the TestComposite alive in the test scope (dropping it destroys the containers):
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
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.
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:
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
# 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
Troubleshooting
"Docker daemon not running" / "Cannot connect to Docker daemon"
Start Docker Desktop or the Docker daemon. pdk-test uses the Docker API via bollard.
Test times out waiting for Omni readiness
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.yamlis valid — an invalid registration causes Omni to exit immediately - Increase timeout:
.timeout(Duration::from_secs(120))
"WASM not found" / policy doesn't apply
- Run
make buildbefore tests - Verify
POLICY_DIRpoints totarget/wasm32-wasip1/release - Verify
POLICY_NAMEmatches the output ofmake show-policy-ref-name
Mock not hit / unexpected 404
- Remember that Omni listens on ALL paths on the configured port. The
.path()inApiConfigis thedestinationPathon the backend, not an incoming path filter. - Verify the mock
whenconditions match what Omni actually forwards to the backend. - Use
--nocaptureto see Omni logs and confirm the policy is loaded.
"No such image" error
Pull the Omni image first:
docker pull mulesoft/flex-gateway:1.10.0
Or override with an available version:
PDK_TEST_FLEX_IMAGE_VERSION=latest cargo test --test requests
Tests pass locally but fail in 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)
"Only 1 HttpMock can be defined per test"
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.
Completion Checklist
After writing integration tests, verify:
-
make buildsucceeds (WASM artifact is fresh) -
tests/config/registration.yamlexists locally (NOT committed to git) -
tests/common/mod.rshas the correctPOLICY_NAME(frommake show-policy-ref-name) -
cargo test --test requestspasses with Docker running - Tests cover at least: one happy-path request, one rejection/error case
How can the creator link this skill?
Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.
<a href="https://skillzs.dev/skills/mulesoft/mulesoft-dx/pdk-test">View pdk-test on skillZs</a>