[dev-dependencies]
pdk-unit = { version = "1.8" }
Writing Unit Tests
Use the pdk-unit testing framework for Flex 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 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-wasmhost 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:
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:
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:
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"));
}
This example always returns 200.
|
Return Custom Upstream Responses
Use UnitHttpResponse or a different closure that implements the Backend to mock upstream responses:
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:
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:
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.
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:
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);
}
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:
| Method | Signature | Description |
|---|---|---|
|
|
Sets the policy configuration as a JSON string. |
|
|
Sets the default HTTP backend when no named upstream matches the request. Defaults to a |
|
|
Registers an HTTP upstream matched by host authority, for example, |
|
|
Registers an HTTP upstream by its full internal service name. Use |
|
|
Registers a gRPC upstream matched by host authority. The authority is converted to the internal upstream service name. |
|
|
Registers a gRPC upstream by its full internal service name. |
|
|
Configures the identity management service URL and its HTTP backend mock. Use when the policy performs OAuth token introspection or similar identity calls. |
|
|
Mutates the |
|
|
Consumes the builder and creates a |
Core API Reference
| Type / function | Description |
|---|---|
|
Fluent builder. Configure the policy, metadata, and backends, then call |
|
Main orchestrator. Use |
|
In-flight request handle. |
|
HTTP request built with method helpers ( |
|
HTTP response from |
|
Traits for both |
|
gRPC wrappers for |
|
Trait for HTTP upstream mocks. Implemented for closures and for structs whose values convert to |
|
Trait for gRPC upstream mocks. Implemented for closures via the |
|
Wraps any |
|
Proc-macro that derives |
|
Controls ordering when resuming paused requests. Enabled with the |



