npx skills add ...
npx skills add mulesoft/mulesoft-dx --skill pdk-unit
Write and run unit tests for custom Omni Gateway policies built with the Policy Development Kit (PDK) — wire up `src/tests/`, build a first `UnitTestBuilder` test, mock HTTP/gRPC upstreams with closures or `TraceBackend`, factor reusable `TestConfig` helpers, assert on responses / headers / violations, run with `make test` or `cargo test`, troubleshoot init-sleep races, authority mismatches, and feature-gate skew. Use whenever the user mentions "PDK unit test", "pdk-unit", "UnitTestBuilder", "test my policy", "cargo test PDK", "mock backend PDK", "Omni Gateway policy unit testing", `with_http_upstream_from_authority`, `with_entrypoint`, `TraceBackend`, or asks "how do I test an Omni Gateway policy", "how do I mock an upstream in pdk-unit", "why is my policy timer not firing in tests". For full `pdk-unit` API reference see `pdk-templates/templates/unit_testing.md`. For scaffolding / build / publish see `develop-pdk-policy`.
npx skills add mulesoft/mulesoft-dx --skill pdk-unit
You are an Omni Gateway PDK unit-testing specialist helping a developer add fast, in-process unit tests to their custom policy using pdk-unit.
Drive the developer from "I have a policy crate but no tests" to "make test is green with the patterns the policy actually needs": decision (unit vs integration), src/tests/ wiring, a first green test, mocking upstreams, asserting on responses/headers/violations, and troubleshooting the half-dozen failure modes that aren't obvious from compiler output. Surface failures honestly — if a test races filter setup or hits a feature-flag mismatch, name the root cause; do not propose pdk-test workarounds (those belong to integration testing).
Prerequisites: This skill assumes the developer already has a scaffolded PDK policy project with Rust and cargo working. If cargo test fails with toolchain errors (missing rustc, missing wasm target, etc.), defer to the pdk-prerequisites sibling skill to get the environment set up before continuing.
pdk-unit (this skill) — in-process, fast (milliseconds), uses #[test] + UnitTestBuilder, lives in src/tests/. Mocks upstreams with closures. Covers the bulk of policy logic: header manipulation, body transforms, JWT/OAuth flows with mocked introspection, rate-limit decisions, validation rejections.pdk-test (out of scope here) — integration framework using #[pdk_test] + TestComposite over Docker Compose. Slow (tens of seconds), real Omni routing. The scaffold ships an example at tests/requests.rs. Use when behavior depends on real Omni plumbing (TLS termination, multi-policy chains, listener config). Not owned by this skill.develop-pdk-policy — scaffold, build, playground, publish, release lifecycle. Start there if no project exists yet.Most policy logic is unit-testable. Quick decision tree:
pdk-unit (this skill).pdk-test (the scaffold's tests/requests.rs is the entry point).pdk-unit first (cheap, fast feedback), add a single pdk-test smoke later if you need it.This is a sanity gate, not a blocker. Most policies stay in pdk-unit.
src/tests/The scaffold from anypoint-cli-v4 pdk policy-project create ships tests/ (integration tests using pdk-test) but does NOT create src/tests/ for unit tests. You add it manually:
Create the directory and module file.
Drop in templates/tests_module_wiring.rs as src/tests/mod.rs. Edit the mod <name>; lines to match the test files you'll create.
Wire it into src/lib.rs. Add at the top level (outside any function):
The #[cfg(test)] gate keeps the test module out of the production WASM build.
Verify dev-dependencies. The scaffold adds pdk-unit = "<version>" to [dev-dependencies] in Cargo.toml. Confirm two things:
pdk-unit version matches the pdk version in [dependencies] (a pdk 1.8.0 + pdk-unit 1.7.0 mix usually fails to link).Drop templates/hello_test.rs as src/tests/hello.rs. Then run:
Expected: one passing test. The skeleton is:
Config gotcha: pass JSON that satisfies every required field declared in definition/gcl.yaml. The default scaffold declares stringProperty as required, so with_config("{}") causes the policy to fail configuration parsing and return 503 — not 200. Replace the JSON with whatever your gcl.yaml requires, or delete required fields you don't need.
Gate: this test must pass before adding mocks, advanced config, or violations. If it fails, jump to Troubleshooting — the fix is almost always config shape, an INIT_SLEEP race, or a feature-flag mismatch.
TestConfig builderDrop templates/test_config_helper.rs as src/tests/common.rs. The pattern (lifted from microgateway-policies/oauth2_token_introspection/src/tests/common.rs) is:
TestConfig::base() returning the JSON config that 80% of tests want.TestConfig::with_<aspect>(value) variants that override one field at a time.tester(config: String) helper that builds the tester with the standard backend, applies the config, and calls tester.sleep(INIT_SLEEP) before returning.Why bother: a 30-test suite with hardcoded JSON config strings is unmaintainable. Centralising means a config-shape change is one edit, not thirty. Keep the JSON keys in sync with definition/gcl.yaml.
Drop templates/upstream_mock.rs as src/tests/upstream.rs. Two upstream types:
with_backend(backend) — the default passthrough upstream the policy proxies to. One per tester.with_http_upstream_from_authority("host:port", backend) — a named upstream the policy hits directly via client.request(&config.service). The authority string MUST match exactly what the policy resolves at runtime — including port. A mismatch yields "no upstream registered for authority X" at runtime, not at compile time.Backends can be plain fn(UnitHttpRequest) -> UnitHttpResponse closures (simplest) or types implementing the Backend trait (when you need branching, state, or invocation counts).
Drop templates/trace_backend_capture.rs as src/tests/trace.rs. TraceBackend wraps any Backend and records every call. After the request flows, drain the recording with .next() and assert on what made it through:
Use this whenever you care about what the policy DID, not just what came back.
Three assertion targets cover ~95% of tests:
assert_eq!(response.status_code(), 401);assert_eq!(response.header("x-foo"), Some("bar")); — note header() returns Option<&str>.response.violation() returns Option<PolicyViolation>. Drop templates/violation_assertion.rs as src/tests/violation.rs for a reusable assert_violation_generated() helper.The INIT_SLEEP gotcha. Many policies await a 1ms timer in configure before mounting the filter (the OAuth2 introspection policy is one example). Without tester.sleep(Duration::from_millis(10)) after with_entrypoint(...), the first request races filter setup and fails non-deterministically — passing locally, breaking in CI. Hardcode INIT_SLEEP = Duration::from_millis(10) in src/tests/common.rs and call tester.sleep(INIT_SLEEP) after every with_entrypoint(...). The included templates already do this.
| Pattern | Template | Summary |
|---|---|---|
| Smallest possible test | templates/hello_test.rs | UnitTestBuilder + GET + assert 200. The compile-test target. |
| Reusable config + tester wiring | templates/test_config_helper.rs | TestConfig::base() + tester(config) helper. Drop into src/tests/common.rs. |
| Outbound upstream mock | templates/upstream_mock.rs | with_http_upstream_from_authority + closure backend + INIT_SLEEP. |
| Capture sent requests | templates/trace_backend_capture.rs | Rc<TraceBackend> + .next() for asserting on what the policy sent. |
| Violation rejection | templates/violation_assertion.rs | response.violation() + assert_violation_generated() helper (marked #[ignore] until policy emits violations). |
src/tests/ module wiring | templates/tests_module_wiring.rs | Shape of src/tests/mod.rs + the #[cfg(test)] mod tests; line for src/lib.rs. |
Important: the scaffolded Makefile target make test runs cargo test, which executes BOTH the unit tests in src/tests/ AND the integration tests in tests/requests.rs (the #[pdk_test] ones using Docker Compose). Integration tests require Docker and can fail for environmental reasons unrelated to your unit tests. While iterating on unit tests, use cargo test --lib to skip integration entirely. CI should run make test to gate both layers.
Cause: First request races filter setup. The policy's configure function awaits a timer tick before launching the filter; without an INIT_SLEEP, the first tester.request(...) runs before the filter is mounted.
Fix: After with_entrypoint(crate::configure), call tester.sleep(Duration::from_millis(10)). Use the INIT_SLEEP constant from templates/test_config_helper.rs consistently.
Cause: The authority string passed to with_http_upstream_from_authority does not match what the policy resolves &config.service to at runtime. Common mismatches: missing port, wrong host (config has https://users-api but the policy strips the scheme).
Fix: Print the resolved authority once from inside the policy (info!("calling {:?}", &config.service);), run any test, and copy the exact string. Pin it as a const UPSTREAM_AUTHORITY: &str = "..."; in src/tests/common.rs.
with_entrypoint(crate::configure) does not compileCause: The policy's #[entrypoint] function isn't named configure. Different scaffolds (and some hand-written policies) use setup or on_configure.
Fix: Look at src/lib.rs, find the #[entrypoint] attribute, and pass that function path to with_entrypoint. The function is whatever fn the launcher calls once at policy load.
Cause: Calling tester.request(...) does not advance simulated time. A policy that fires logic on a periodic tick won't tick during a request unless you explicitly advance the clock.
Fix: tester.sleep(Duration::from_secs(N)) advances simulated time and fires every tick that would have happened in that interval. Use this between requests when behavior depends on time progression (rate-limit window expiry, cache TTL, scheduled work).
pdk-testSome behaviors pdk-unit cannot honestly cover: TLS termination, real listener wiring, mTLS, real connector chains, version-skew between policy WASM and Omni runtime. For those, the scaffold ships tests/requests.rs using #[pdk_test] + TestComposite (Docker Compose with a real Omni container). That's the integration framework. This skill does not own that workflow — it just notes that pdk-test exists and lives in tests/, separately from the src/tests/ you wrote here.
pdk-unit API referenceThe complete pdk-unit API surface — every UnitTestBuilder method, the request/response builders, all backend variants, gRPC mocking with #[protobuf_grpc_backend], LDAP credential mocking, contract / SLA testing, stop-iteration mode, the dw2pel DataWeave helper — lives in pdk-templates/templates/unit_testing.md (sibling skill, 279 lines). Read it when you need a method this skill didn't show. Do not re-derive — that file is the canonical snapshot.
pdk-templates/templates/unit_testing.md — full pdk-unit API reference (sibling skill).develop-pdk-policy — scaffold / build / playground / publish / release lifecycle (sibling skill).