Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Testing

Volter provides a TestClient in the volter-testing crate. It lets you write integration tests against your router without starting an HTTP server.

Adding as a Dependency

volter-testing is a workspace crate. In a published project, add it as a dev-dependency:

[dev-dependencies]
volter-testing = { git = "https://github.com/anomalyco/volter" }

(It is not yet published separately or re-exported from the volter crate.)

Basic Usage

#![allow(unused)]
fn main() {
use volter::*;
use volter_testing::TestClient;

#[tokio::test]
async fn test_hello_endpoint() {
    let app = Router::new().route("/", get(hello));
    let client = TestClient::new(app);

    let response = client.get("/").send().await;
    assert_eq!(response.status(), StatusCode::OK);
}
}

Testing Status Codes

#![allow(unused)]
fn main() {
#[tokio::test]
async fn test_not_found() {
    let app = Router::new().route("/", get(hello));
    let client = TestClient::new(app);

    let response = client.get("/nonexistent").send().await;
    assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
}

Testing Response Bodies

#![allow(unused)]
fn main() {
use serde::{Deserialize, Serialize};

#[derive(Serialize)]
struct UserResponse {
    id: u64,
    name: String,
}

#[tokio::test]
async fn test_json_response() {
    let app = Router::new().route("/user", get(user_handler));
    let client = TestClient::new(app);

    let response = client.get("/user").send().await;
    assert_eq!(response.status(), StatusCode::OK);

    let body: serde_json::Value = response.json().await.unwrap();
    assert_eq!(body["name"], "Alice");
}
}

Sending Request Bodies

#![allow(unused)]
fn main() {
use serde::Deserialize;

#[derive(Deserialize)]
struct CreateResponse {
    id: u64,
}

#[tokio::test]
async fn test_create_user() {
    let app = Router::new().route("/users", post(create_user));
    let client = TestClient::new(app);

    let response = client
        .post("/users")
        .json(&serde_json::json!({"name": "Alice"}))
        .send()
        .await;

    assert_eq!(response.status(), StatusCode::CREATED);

    let body: CreateResponse = response.json().await.unwrap();
    assert!(body.id > 0);
}
}

Testing Headers

Set custom headers on the request and check headers on the response:

#![allow(unused)]
fn main() {
#[tokio::test]
async fn test_custom_header() {
    let app = Router::new().route("/", get(echo_header));
    let client = TestClient::new(app);

    let response = client
        .get("/")
        .header(http::header::AUTHORIZATION, "Bearer token123".parse().unwrap())
        .send()
        .await;

    assert_eq!(
        response.headers().get(http::header::CONTENT_TYPE),
        Some(&http::HeaderValue::from_static("application/json"))
    );
}
}

Testing with State

#![allow(unused)]
fn main() {
#[derive(Clone)]
struct AppState {
    counter: u64,
}

#[tokio::test]
async fn test_with_state() {
    let app = Router::with_state(AppState { counter: 42 })
        .route("/", get(handler));
    let client = TestClient::new(app);
    // ...
}
}

Testing Middleware

Test that middleware behaves correctly:

#![allow(unused)]
fn main() {
#[tokio::test]
async fn test_timeout() {
    use std::time::Duration;

    async fn slow_handler() -> &'static str {
        tokio::time::sleep(Duration::from_secs(10)).await;
        "done"
    }

    let app = Router::new()
        .route("/slow", get(slow_handler))
        .layer(TimeoutLayer::new(Duration::from_millis(10)));

    let mut client = TestClient::new(app);
    let response = client.get("/slow").send().await;
    assert_eq!(response.status(), StatusCode::REQUEST_TIMEOUT);
}
}

The Testing API

TestClient

#![allow(unused)]
fn main() {
impl<S: Clone + Send + 'static> TestClient<S> {
    pub fn new(router: Router<S>) -> Self;
    pub fn get(&self, path: &str) -> TestRequestBuilder<S>;
    pub fn post(&self, path: &str) -> TestRequestBuilder<S>;
    pub fn request(&self, method: Method, path: &str) -> TestRequestBuilder<S>;
}
}

TestRequestBuilder

#![allow(unused)]
fn main() {
impl TestRequestBuilder {
    pub fn header(self, name: HeaderName, value: HeaderValue) -> Self;
    pub fn json<T: Serialize>(self, value: &T) -> Self;
    pub fn body(self, body: impl Into<Bytes>) -> Self;
    pub async fn send(self) -> TestResponse;
}
}

TestResponse

#![allow(unused)]
fn main() {
impl TestResponse {
    pub fn status(&self) -> StatusCode;
    pub fn headers(&self) -> &HeaderMap;
    pub async fn bytes(self) -> Result<Bytes, BodyError>;
    pub async fn text(self) -> Result<String, BodyError>;
    pub async fn json<T: DeserializeOwned>(self) -> Result<T, BodyError>;
}
}