Cookbook / Production

Testing your app

Call routes in-process, with no sockets and no ports, and assert on status, headers and body.

Server::into_app() builds the full pipeline (routing, middleware, error handling) so tests run requests through exactly what production runs, without binding a port. Write a function that builds your Server, and use it from both main and your tests.

src/main.rs
use axeon::{Json, Server, ServerError};
use axeon::json;
use serde::Deserialize;

#[derive(Deserialize)]
struct Greeting {
    name: String,
}

pub fn app() -> Server {
    let mut app = Server::new();
    app.get("/hello/:name", |req| async move {
        format!("Hello, {}!", req.params()["name"])
    });
    app.post("/greet", |req| async move {
        let greeting: Greeting = req.json()?;
        Ok::<_, ServerError>((201, Json(json!({ "greeting": format!("Hi {}", greeting.name) }))))
    });
    app
}

fn main() {
    app().listen("127.0.0.1:3000").expect("server failed to start");
}

#[cfg(test)]
mod tests {
    use super::*;
    use axeon::{Method, Request};

    #[tokio::test]
    async fn says_hello() {
        let app = app().into_app().await.unwrap();
        let res = app.call(Request::new(Method::GET, "/hello/Ada")).await;
        assert_eq!(res.status, 200);
        assert_eq!(res.body_text(), "Hello, Ada!");
    }

    #[tokio::test]
    async fn greets_with_json() {
        let app = app().into_app().await.unwrap();
        let req = Request::new(Method::POST, "/greet").with_json(&json!({ "name": "Grace" }));
        let res = app.call(req).await;
        assert_eq!(res.status, 201);
        assert!(res.body_text().contains("Hi Grace"));
    }

    #[tokio::test]
    async fn rejects_bad_input() {
        let app = app().into_app().await.unwrap();
        let req = Request::new(Method::POST, "/greet").with_json(&json!({ "nope": 1 }));
        assert_eq!(app.call(req).await.status, 422);
    }

    #[tokio::test]
    async fn unknown_routes_are_404() {
        let app = app().into_app().await.unwrap();
        assert_eq!(app.call(Request::new(Method::GET, "/missing")).await.status, 404);
    }
}
Terminal
cargo test

Notes#

  • Add tokio = { version = "1", features = ["full"] } to [dev-dependencies] for #[tokio::test].
  • Request::new(..).with_header(..) and .with_body(content_type, bytes) build anything else you need, such as an Authorization header.
  • App::into_service() turns the app into a tower Service, which is handy to mount inside another framework.