Fast, reliable tests need to drive your app the way a client does — without
booting a server or touching the network. Rustango's TestClient runs your
router in-process: you call client.get("/path"), it routes the request
through the real stack (extractors, middleware, handlers) and hands back the
response to assert on. Add transaction-rollback isolation for database tests and
a set of response assertions, and you have Django's test client + TestCase, in
Rust.
New to a term here? router, handler, fixture, rollback — see the glossary.
Source:
rustango::test_client(TestClient,TestResponse),rustango::test_assertions(assert_status_2xx,assert_redirects,assert_cookie_set, …), andrustango::test_db(with_rollback) — always compiled.Runnable version: the snippets below are a passing test —
testing_doc.rs(cargo test -p rustango --test testing_doc). Nearly every other*_doc.rsin this repo usesTestClientthe same way.
- Step 1 — Drive your app with TestClient
- Step 2 — Assert on the response
- Sending JSON, headers, and bodies
- Testing a real API
- Database tests with rollback
- Response assertion helpers
- See also
Wrap any axum::Router in a TestClient and send requests — no socket is bound,
no server task spawned. The request flows through your real middleware and
handlers:
use rustango::test_client::TestClient;
let client = TestClient::new(app()); // app() returns your Router
let res = client.get("/ping").send().await; // routed in-process
assert_eq!(res.status, 200);TestClient has get / post / put / patch / delete / head, each
returning a builder you finish with .send().await.
TestResponse exposes the status and body in whatever shape you need:
let res = client.get("/ping").send().await;
res.status; // u16 — e.g. 200
res.text(); // body as a String
res.header("content-type"); // Option<&str>// JSON, two ways:
let res = client.post("/echo").json(&json!({ "name": "Ada" })).send().await;
assert_eq!(res.json_value()["name"], "Ada"); // untyped
#[derive(serde::Deserialize)]
struct Out { name: String }
let out: Out = res.json(); // typed
assert_eq!(out.name, "Ada");The request builder chains everything before .send():
let res = client
.post("/api/posts")
.header("authorization", "Bearer <token>") // auth, content negotiation, …
.json(&json!({ "title": "Hello", "body": "..." }))
.send()
.await;
assert_eq!(res.status, 201);Use .body(...) for raw (non-JSON) bodies, and a missing route returns a real
404 — verified in the backing test.
app() in your tests is just your router. For a DB-backed API, build it exactly
as main.rs does but with a test pool — the pattern most *_doc.rs tests use:
async fn app() -> axum::Router {
let pool = test_pool().await; // a sqlite::memory: or test DB pool
PostViewSet::router("/api/posts", pool)
}
#[tokio::test]
async fn create_then_list() {
let client = TestClient::new(app().await);
let created = client.post("/api/posts")
.json(&json!({ "title": "Hi", "body": "b" }))
.send().await;
assert_eq!(created.status, 201);
let list = client.get("/api/posts").send().await;
assert!(list.json_value()["results"].is_array());
}This is the ViewSets test from that guide — the same TestClient.
Tests that write to a database must not leak state into each other.
test_db::with_rollback runs your test inside a transaction and rolls it back
at the end, so every test starts from the same clean state and nothing persists:
use rustango::test_db::with_rollback;
#[tokio::test]
async fn creating_a_post_persists_it() {
with_rollback(&pool, |tx| async move {
// ... insert + assert against `tx` ...
// everything here is rolled back when the closure returns
}).await;
}For SQLite, the *_sqlite_live.rs tests throughout this repo use an in-memory
database per test instead — also fully isolated, with zero external setup.
For raw axum::Response values (e.g. from tower::oneshot), test_assertions
reads like Django's assertContains / assertRedirects:
use rustango::test_assertions::{assert_status_2xx, assert_redirects, assert_cookie_set};
assert_status_2xx(&res);
assert_redirects(&res, "/login?next=/dashboard");
assert_cookie_set(&res, "rustango_session", None);Also available: assert_status / assert_status_in / assert_status_4xx /
assert_status_5xx, assert_header, assert_content_type,
assert_redirect_chain, assert_cookie_not_set, and assert_messages.
- ViewSets · HTML views — what you point the
TestClientat. - Middleware —
TestClientexercises the layers too (DB-free, viatower::oneshotinmiddleware.rs). - Getting started — Step 16 writes the first test.
manageCLI —make:testscaffolds a test module.
