Test Client
TestClient<A> wraps any type that implements Application and dispatches requests through Application::execute in-process. No network, no port binding, no spawned threads — tests run as fast as plain function calls.
Creating a client
use rust_web_server::app::App;use rust_web_server::core::New;use rust_web_server::test_client::TestClient;
let client = TestClient::new(App::new());Every request is dispatched on a synthetic 127.0.0.1:12345 → 127.0.0.1:7878 connection. Pass any Application implementation — App, AppWithState, WithMiddleware, McpServer, or your own type.
State-aware applications
use std::sync::Arc;use rust_web_server::app::App;use rust_web_server::test_client::TestClient;use rust_web_server::routes;
struct AppState { counter: u32 }
let state = Arc::new(AppState { counter: 42 });let app = routes! { App::with_state(Arc::clone(&state)), GET "/count" => |_req, _params, _conn, s: &Arc<AppState>| { // ... build response },};let client = TestClient::new(app);Building requests
Each HTTP method has a corresponding builder method on TestClient:
client.get("/users")client.post("/users")client.put("/users/1")client.patch("/users/1")client.delete("/users/1")client.options("/users")All return a TestRequest that you configure with chained builder calls before calling .send().
Adding headers
let resp = client.get("/api/data") .header("Authorization", "Bearer my-token") .header("Accept", "application/json") .send();Setting the body
// Raw byteslet resp = client.post("/upload") .header("Content-Type", "application/octet-stream") .body_bytes(vec![0x89, 0x50, 0x4E, 0x47]) .send();
// UTF-8 textlet resp = client.post("/echo") .header("Content-Type", "text/plain") .body_text("hello world") .send();
// JSON (set Content-Type yourself)let resp = client.post("/users") .header("Content-Type", "application/json") .body_text(r#"{"name":"Alice","email":"alice@example.com"}"#) .send();Reading the response
.send() returns a TestResponse:
let resp = client.get("/healthz").send();
resp.status() // i16 — e.g. 200resp.reason() // &str — e.g. "OK"resp.is_success() // bool — true when 200–299resp.body_text() // &str — panics if body is not valid UTF-8resp.body_bytes() // &[u8] — raw bodyresp.header("content-type") // Option<&str> — case-insensitive lookupresp.headers() // &[Header] — all response headersTesting middleware
Wrap any application with middleware before handing it to TestClient:
use rust_web_server::app::App;use rust_web_server::core::New;use rust_web_server::rate_limit::RateLimitLayer;use rust_web_server::test_client::TestClient;
let app = App::new().wrap(RateLimitLayer::new(5, 60)); // 5 req / 60 slet client = TestClient::new(app);
// First five requests succeedfor _ in 0..5 { assert_eq!(200, client.get("/healthz").send().status());}
// Sixth request is rate-limitedassert_eq!(429, client.get("/healthz").send().status());Complete CRUD test suite
#[cfg(test)]mod tests { use rust_web_server::app::App; use rust_web_server::core::New; use rust_web_server::test_client::TestClient;
fn make_client() -> TestClient<App> { TestClient::new(App::new()) }
#[test] fn health_check_returns_200() { let client = make_client(); let resp = client.get("/healthz").send(); assert_eq!(200, resp.status()); }
#[test] fn unknown_route_returns_404() { let client = make_client(); let resp = client.get("/does-not-exist").send(); assert_eq!(404, resp.status()); }
#[test] fn post_creates_resource() { let client = make_client(); let resp = client .post("/users") .header("Content-Type", "application/json") .body_text(r#"{"name":"Alice","email":"alice@example.com"}"#) .send(); assert_eq!(201, resp.status()); assert!(resp.header("location").is_some()); }
#[test] fn get_returns_json_content_type() { let client = make_client(); let resp = client .get("/users/1") .header("Accept", "application/json") .send(); assert_eq!(200, resp.status()); let ct = resp.header("content-type").unwrap_or(""); assert!(ct.contains("application/json"), "content-type was: {ct}"); }
#[test] fn put_updates_resource() { let client = make_client(); let resp = client .put("/users/1") .header("Content-Type", "application/json") .body_text(r#"{"name":"Alice Smith"}"#) .send(); assert_eq!(200, resp.status()); }
#[test] fn delete_removes_resource() { let client = make_client(); let resp = client.delete("/users/1").send(); assert!(resp.status() == 200 || resp.status() == 204); }
#[test] fn body_text_is_accessible() { let client = make_client(); let resp = client.get("/healthz").send(); let body = resp.body_text(); // body is a &str; check it is non-empty or contains expected content assert!(!body.is_empty() || resp.status() == 200); }}