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

# 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.8" }
```

## 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 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>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>
