Skip to content

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 bytes
let resp = client.post("/upload")
.header("Content-Type", "application/octet-stream")
.body_bytes(vec![0x89, 0x50, 0x4E, 0x47])
.send();
// UTF-8 text
let 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. 200
resp.reason() // &str — e.g. "OK"
resp.is_success() // bool — true when 200–299
resp.body_text() // &str — panics if body is not valid UTF-8
resp.body_bytes() // &[u8] — raw body
resp.header("content-type") // Option<&str> — case-insensitive lookup
resp.headers() // &[Header] — all response headers

Testing 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 s
let client = TestClient::new(app);
// First five requests succeed
for _ in 0..5 {
assert_eq!(200, client.get("/healthz").send().status());
}
// Sixth request is rate-limited
assert_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);
}
}