---
schema_version: "1"
id: f095d728748148a140366c0b5c3ef3a0023fcff017592970b463e91824d0faac
title: Writing Unit Tests
collection: mulesoft
version: current
locale: en-us
portal_url: https://docs.salesforce.com/mulesoft/current/en-us/pdk/latest/policies-pdk-unit.md
canonical_url: https://docs.mulesoft.com/pdk/latest/policies-pdk-unit
source_site: docs.mulesoft.com
content_type: documentation
content_hash: 4d450a62510675f7f58ba2a9f4453d5c727cbb116f067c8dac7832a34ac20a3a
description: MuleSoft Documentation Site
release: "1.10"
latest: true
---
---
product: Omni Gateway Policy Development Kit (PDK)
version: 1.10
is-latest-version: true
---

# Writing Unit Tests

> For the full documentation index, see: https://docs.mulesoft.com/llms.txt

Use the `pdk-unit` testing framework for Omni Gateway Policy Development Kit (PDK) custom policies
to test the full request and response lifecycle of a policy without a running Envoy proxy, a
WebAssembly runtime, or external infrastructure.

To see example `pdk-unit` tests, browse the
[PDK Custom Policy Examples](https://github.com/mulesoft/pdk-custom-policy-examples) on GitHub.

Use `pdk-unit` when you need tests that are:

- **Fast feedback**: Tests run as ordinary Rust unit tests completing in milliseconds.

- **Deterministic**: No network, no containers, no race conditions.

- **Full lifecycle coverage**: Exercises request headers, request body, response headers, and
  response body phases as the real `proxy-wasm` host does.

- **Code coverage**: Policy code runs directly in the Rust environment, so it works with standard
  coverage tools such as `cargo-tarpaulin`.

- **Debugging**: Tests run as native Rust code, so you can attach a debugger or profiler without
  extra setup.

## Enable pdk-unit

Add `pdk-unit` as a dev-dependency in your policy’s `Cargo.toml`:

```toml
[dev-dependencies]
pdk-unit = { version = "1.10" }
```

### Enable pdk-unit for Non-PDK Policies

PDK-Unit supports testing any policy built with
[proxy-wasm-rust-sdk](https://github.com/proxy-wasm/proxy-wasm-rust-sdk). You can write and run unit
tests for existing proxy-wasm policies without migrating them to PDK first. Use this functionality
to:

- Validate behavior before migration: Test your legacy proxy-wasm policies to establish a behavioral
  baseline.

- Identify regressions during migration: Establish tests before migrating your policy to PDK to make
  sure behavioral differences surface immediately.

- Adopt PDK incrementally: Migrate your policies to PDK at your own pace while still performing unit
  testing.

To test non-PDK policies with pdk-unit:

1.  Make these edits to the custom policy’s `Cargo.toml` file before compilation:

    - Replace the `pdk-proxy-wasm-stub` target instead of using `proxy-wasm`.

      > [!NOTE] Don’t make this edit for `wasm32` architecture.

    - Add the `pdk_unit` dependency.

      ```toml
      [target.'cfg(target_arch = "wasm32")'.dependencies]
      proxy-wasm = "0.2.5"

      [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
      pdk-proxy-wasm-stub = "1.10.0"

      [dev-dependencies]
      pdk_unit = { version = "1.10.0", default-features = false, features = ["proxy-wasm-rust-sdk"] }
      ```

2.  Conditionally import the appropriate crate in your policy code:

    ```rust
    #[cfg(target_arch = "wasm32")]
    use proxy_wasm as pw;
    #[cfg(not(target_arch = "wasm32"))]
    use pdk_proxy_wasm_stub as pw;
    ```

3.  Write your tests as you would for a PDK policy. However, use the `with_context` method instead
    of the `with_entrypoint` method. The `with_context` method receives a function that creates a
    root context of the root filter:

    ```rust
    let mut tester = UnitTestBuilder::default()
        .with_backend(TraceBackend::new(UnitHttpResponse::upgrade()))
        .with_http_upstream("name.namespace.svc", |req: UnitHttpRequest| {
            UnitHttpResponse::new(200)
        })
        .with_context(|| Box::new(WebSocketRoot));
    ```

## Basic Request and Response Test

To send a test request build a `UnitTest` with the `UnitTestBuilder`, send a `UnitHttpRequest`, and
assert on the returned `UnitHttpResponse`. Headers and the body are available through the
`UnitHttpMessage` trait:

```rust
use pdk_unit::{UnitTestBuilder, UnitHttpRequest, UnitHttpMessage};

#[test]
fn test_policy_allows_get() {
    let mut tester = UnitTestBuilder::default()
        .with_config(r#"{"allowed_methods": ["GET"]}"#)
        .with_entrypoint(crate::configure);

    let response = tester.request(
        UnitHttpRequest::get().with_path("/api/resource"),
    );

    assert_eq!(response.status_code(), 200);
}
```

The `request` method drives the request until execution finishes and returns a `UnitHttpResponse`.
If the request doesn’t resolve immediately, the test advances the internal clock and retries until
the request completes.

## View Backend Requests

Use `TraceBackend` to record requests that reach the upstream. Supply any `Backend` as the response
generator:

```rust
use pdk_unit::{UnitTestBuilder, UnitHttpRequest, UnitHttpResponse, UnitHttpMessage, TraceBackend, Backend};
use std::rc::Rc;

#[test]
fn test_policy_adds_header() {
    let backend = Rc::new(TraceBackend::new(UnitHttpResponse::new(200)));

    let mut tester = UnitTestBuilder::default()
        .with_config(r#"{}"#)
        .with_backend(Rc::clone(&backend))
        .with_entrypoint(crate::configure);

    tester.request(UnitHttpRequest::get().with_path("/api/resource"));

    let upstream_req = backend.next().unwrap();
    assert_eq!(upstream_req.header("x-added-by-policy"), Some("true"));
}
```

> [!NOTE] This example always returns `200`.

## Return Custom Upstream Responses

Use `UnitHttpResponse` or a different closure that implements the `Backend` to mock upstream
responses:

```rust
use pdk_unit::{UnitTestBuilder, UnitHttpRequest, UnitHttpResponse, UnitHttpMessage};

#[test]
fn test_policy_handles_upstream_error() {
    let mut tester = UnitTestBuilder::default()
        .with_config(r#"{}"#)
        .with_backend(|_req| {
            UnitHttpResponse::new(503)
                .with_body(r#"{"error":"unavailable"}"#)
        })
        .with_entrypoint(crate::configure);

    let response = tester.request(
        UnitHttpRequest::get().with_path("/api/resource"),
    );

    assert_eq!(response.status_code(), 503);
}
```

## Mock an HTTP Upstream

When a policy issues an HTTP call to a specific authority, register a backend for that authority by
using `with_http_upstream_from_authority`:

```rust
use pdk_unit::{UnitTestBuilder, UnitHttpRequest, UnitHttpResponse, UnitHttpMessage};

#[test]
fn test_policy_calls_external_service() {
    let mut tester = UnitTestBuilder::default()
        .with_config(r#"{"auth_url": "https://auth.example.com"}"#)
        .with_http_upstream_from_authority("auth.example.com", |_req| {
            UnitHttpResponse::new(200)
                .with_body(r#"{"valid":true}"#)
                .into()
        })
        .with_entrypoint(crate::configure);

    let response = tester.request(
        UnitHttpRequest::get().with_path("/secure"),
    );

    assert_eq!(response.status_code(), 200);
}
```

## Mock a gRPC upstream

Use the `protobuf_grpc_backend` macro to derive a `GrpcBackend` implementation from a Rust struct
with annotated methods:

```rust
use pdk_unit::{UnitTestBuilder, UnitHttpRequest, UnitHttpMessage, protobuf_grpc_backend};

#[derive(Default)]
pub struct MockAuthService;

#[protobuf_grpc_backend]
impl MockAuthService {
    #[grpc_method(service = "AuthService", method = "Verify")]
    fn verify(&self, _req: VerifyRequest) -> VerifyResponse {
        VerifyResponse { authorized: true, ..Default::default() }
    }
}

#[test]
fn test_grpc_policy() {
    let mut tester = UnitTestBuilder::default()
        .with_config(r#"{}"#)
        .with_grpc_upstream_from_authority("grpc.backend.com", MockAuthService::default())
        .with_entrypoint(crate::configure);

    let response = tester.request(
        UnitHttpRequest::post().with_path("/api/data"),
    );

    assert_eq!(response.status_code(), 200);
}
```

## Test WebSocket Policies

To test custom policies that support WebSocket APIs, use the WebSocket testing capabilities in
`pdk-unit`. WebSocket tests differ from standard HTTP tests because they establish an upgrade
connection and test bidirectional frame exchange.

To test a WebSocket policy:

1.  Configure the backend as `UnitHttpResponse::upgrade()` to simulate a successful WebSocket
    upgrade.

2.  Call the `upgrade` method with `UnitHttpRequest::upgrade()` instead of `request`.

3.  The result provides an `UpgradeConnection` with the upgrade response and handles to send frames
    in both directions.

```rust
use pdk_unit::{UnitTestBuilder, UnitHttpRequest, UnitHttpResponse, UnitFrame, UnitFrameType};

#[test]
fn websocket_non_text_frames_are_forwarded_unchanged() {
    let mut tester = UnitTestBuilder::default()
        .with_backend(UnitHttpResponse::upgrade())
        .with_entrypoint(crate::configure);

    let conn = tester.upgrade(UnitHttpRequest::upgrade()).unwrap();

    assert_eq!(conn.response().header("custom"), Some("Some"));

    // Test client to server frames
    conn.client().send_to_server(UnitFrame::ping());
    let frame = conn.server().next();
    assert!(frame.is_some());
    assert_eq!(frame.unwrap().frame_type(), UnitFrameType::Ping);

    // Test server to client frames
    conn.server().send_to_client(UnitFrame::pong());
    let frame = conn.client().next();
    assert!(frame.is_some());
    assert_eq!(frame.unwrap().frame_type(), UnitFrameType::Pong);
}
```

The `UpgradeConnection` provides:

- `response()`: Returns the upgrade response for asserting headers or status.

- `client()`: Handle to send frames from client to server and receive frames from server.

- `server()`: Handle to send frames from server to client and receive frames from client.

Use `UnitFrame` to construct WebSocket frames for testing:

- `UnitFrame::text(payload)`: Creates a text frame.

- `UnitFrame::binary(payload)`: Creates a binary frame.

- `UnitFrame::ping()`: Creates a ping control frame.

- `UnitFrame::pong()`: Creates a pong control frame.

- `UnitFrame::close()`: Creates a close control frame.

## Test Client ID Enforcement

`add_contract_data` seeds the built-in Anypoint Platform stub with a registered API contract to test
policies that validate client credentials.

```rust
use pdk_unit::{UnitTestBuilder, UnitHttpRequest, UnitHttpMessage};

#[test]
fn test_valid_client_id() {
    let mut tester = UnitTestBuilder::default()
        .with_config(r#"{}"#)
        .with_entrypoint(crate::configure);

    tester.add_contract_data(
        "my-client-id",
        "My App",
        Some("my-client-secret"),
        None::<String>,
    );

    tester.sleep(Duration::from_secs(20)); // advance the internal clock to execute the periodic task

    let response = tester.request(
        UnitHttpRequest::get()
            .with_path("/api/resource")
            .with_header("client_id", "my-client-id")
            .with_header("client_secret", "my-client-secret"),
    );

    assert_eq!(response.status_code(), 200);
}
```

## Test Time-Based Behavior

Unit tests use an internal clock to test timed policy functions. Use the `tick()` and `sleep()`
functions to drive policies that use timers or scheduled callbacks. These functions only move the
internal testing clock forward and don’t actually take the specified time. Use `Clock::now` or
`Timer::now` for time in the policy:

```rust
use pdk_unit::{UnitTestBuilder, UnitHttpRequest, UnitHttpMessage};
use std::time::Duration;

#[test]
fn test_rate_limit_resets_after_window() {
    let mut tester = UnitTestBuilder::default()
        .with_config(r#"{"requests_per_minute": 1}"#)
        .with_entrypoint(crate::configure);

    let req = || UnitHttpRequest::get().with_path("/api/resource");

    let first = tester.request(req());
    assert_eq!(first.status_code(), 200);

    let second = tester.request(req());
    assert_eq!(second.status_code(), 429);

    // Advance time past the rate-limit window
    tester.sleep(Duration::from_secs(60));

    let third = tester.request(req());
    assert_eq!(third.status_code(), 200);
}
```

> [!NOTE] Using `SystemTime::now` creates non-deterministic tests.

## `UnitTestBuilder` Methods

`UnitTestBuilder::default()` is the entry point. All methods return `self` for chaining, except
`with_entrypoint` that consumes the builder and returns a `UnitTest`:

<table><colgroup><col> <col> <col></colgroup><thead><tr><th>Method</th><th>Signature</th><th>Description</th></tr></thead><tbody><tr><td><p><code>with_config</code></p></td><td><p><code>with_config(config: impl Into&lt;String&gt;) → Self</code></p></td><td><p>Sets the policy configuration as a JSON string.</p></td></tr><tr><td><p><code>with_backend</code></p></td><td><p><code>with_backend(backend: impl Backend + 'static) → Self</code></p></td><td><p>Sets the default HTTP backend when no named upstream matches the request. Defaults to a <code>TraceBackend</code> returning <code>200</code>.</p></td></tr><tr><td><p><code>with_http_upstream_from_authority</code></p></td><td><p><code>with_http_upstream_from_authority(authority: impl AsRef&lt;str&gt;, backend: impl Backend + 'static) → Self</code></p></td><td><p>Registers an HTTP upstream matched by host authority, for example, <code>api.example.com</code>. The authority is converted to the internal upstream service name. Call any <code>metadata()</code> calls that change <code>policy_name</code> or <code>policy_namespace</code> before calling <code>with_http_upstream_from_authority</code>.</p></td></tr><tr><td><p><code>with_http_upstream</code></p></td><td><p><code>with_http_upstream(upstream: impl Into&lt;String&gt;, backend: impl Backend + 'static) → Self</code></p></td><td><p>Registers an HTTP upstream by its full internal service name. Use <code>with_http_upstream_from_authority</code> unless if you must pass the service name directly.</p></td></tr><tr><td><p><code>with_grpc_upstream_from_authority</code></p></td><td><p><code>with_grpc_upstream_from_authority(authority: impl AsRef&lt;str&gt;, backend: impl GrpcBackend + 'static) → Self</code></p></td><td><p>Registers a gRPC upstream matched by host authority. The authority is converted to the internal upstream service name.</p></td></tr><tr><td><p><code>with_grpc_upstream</code></p></td><td><p><code>with_grpc_upstream(upstream: impl Into&lt;String&gt;, backend: impl GrpcBackend + 'static) → Self</code></p></td><td><p>Registers a gRPC upstream by its full internal service name.</p></td></tr><tr><td><p><code>with_identity_management</code></p></td><td><p><code>with_identity_management(url: impl Into&lt;String&gt;, backend: impl Backend + 'static) → Self</code></p></td><td><p>Configures the identity management service URL and its HTTP backend mock. Use when the policy performs OAuth token introspection or similar identity calls.</p></td></tr><tr><td><p><code>metadata</code></p></td><td><p><code>metadata(f: impl FnOnce(&amp;mut Metadata)) → Self</code></p></td><td><p>Mutates the <code>Metadata</code> injected into the policy context. Call the <code>metadata</code> method before any <code>with_*_upstream_from_authority</code> method as the upstream methods derive the upstream service name from metadata.</p></td></tr><tr><td><p><code>with_entrypoint</code></p></td><td><p><code>with_entrypoint(entrypoint: impl Entrypoint&lt;C, T&gt; + Clone + 'static) → UnitTest</code></p></td><td><p>Consumes the builder and creates a <code>UnitTest</code>. Pass <code>crate::configure</code> as the entrypoint. Consumes the builder.</p></td></tr></tbody></table>

## Core API Reference

<table><colgroup><col> <col></colgroup><thead><tr><th>Type / function</th><th>Description</th></tr></thead><tbody><tr><td><p><code>UnitTestBuilder</code></p></td><td><p>Fluent builder. Configure the policy, metadata, and backends, then call <code>with_entrypoint</code> to produce a <code>UnitTest</code>.</p></td></tr><tr><td><p><code>UnitTest</code></p></td><td><p>Main orchestrator. Use <code>request</code> for synchronous tests or <code>request_partial</code> plus <code>poll</code> for finer control.</p></td></tr><tr><td><p><code>UnitTestRequest</code></p></td><td><p>In-flight request handle. <code>poll()</code> returns <code>Poll::Pending</code> or <code>Poll::Ready(response)</code>.</p></td></tr><tr><td><p><code>UnitHttpRequest</code></p></td><td><p>HTTP request built with method helpers (<code>get()</code>, <code>post()</code>, and so on) or <code>custom()</code>. Use <code>with_path</code>, <code>with_header</code>, and <code>with_body</code> to populate it.</p></td></tr><tr><td><p><code>UnitHttpResponse</code></p></td><td><p>HTTP response from <code>new(status_code)</code>. Exposes <code>status_code()</code> and the <code>UnitHttpMessage</code> trait accessors.</p></td></tr><tr><td><p><code>UnitHttpMessage</code></p></td><td><p>Traits for both <code>UnitHttpRequest</code> and <code>UnitHttpResponse</code>: <code>header()</code>, <code>headers()</code>, <code>body()</code>, <code>property()</code>, <code>authentication()</code>, and <code>violation()</code>.</p></td></tr><tr><td><p><code>UnitGrpcRequest</code> and <code>UnitGrpcResponse</code></p></td><td><p>gRPC wrappers for <code>GrpcBackend</code> implementations.</p></td></tr><tr><td><p><code>Backend</code></p></td><td><p>Trait for HTTP upstream mocks. Implemented for closures and for structs whose values convert to <code>RequestResponse</code>.</p></td></tr><tr><td><p><code>GrpcBackend</code></p></td><td><p>Trait for gRPC upstream mocks. Implemented for closures via the <code>protobuf_grpc_backend</code>.</p></td></tr><tr><td><p><code>TraceBackend</code></p></td><td><p>Wraps any <code>Backend</code>, records each call, and lets you assert on upstream requests with <code>next()</code>.</p></td></tr><tr><td><p><code>protobuf_grpc_backend</code></p></td><td><p>Proc-macro that derives <code>GrpcBackend</code> from annotated <code>impl</code> methods and handles protobuf encode/decode.</p></td></tr><tr><td><p><code>UpgradeConnection</code></p></td><td><p>WebSocket connection handle returned by <code>upgrade()</code>. Provides <code>response()</code> for the upgrade response, <code>client()</code> for client-side frame operations, and <code>server()</code> for server-side frame operations.</p></td></tr><tr><td><p><code>UnitFrame</code></p></td><td><p>WebSocket frame for testing. Create with <code>text()</code>, <code>binary()</code>, <code>ping()</code>, <code>pong()</code>, or <code>close()</code>. Query the type with <code>frame_type()</code>.</p></td></tr><tr><td><p><code>UnitFrameType</code></p></td><td><p>Enum for WebSocket frame types: <code>Text</code>, <code>Binary</code>, <code>Ping</code>, <code>Pong</code>, <code>Close</code>.</p></td></tr><tr><td><p><code>StopIterationMode</code></p></td><td><p>Controls ordering when resuming paused requests. Enabled with the <code>enable_stop_iteration</code> feature (stable since 1.8).</p></td></tr></tbody></table>
