Zero2Prod: Day 1 – Writing tests

Extract the app logic to a function

async fn main() -> std::io::Result<()> {
    let _ = run().await?;
    Ok(())
}

pub fn run() -> Result<Server, std::io::Error> {
    let server = HttpServer::new(|| create_app())
        .bind("127.0.0.1:8080")?
        .run();
    Ok(server)
}

fn create_app() -> App<
    impl ServiceFactory<
        ServiceRequest,
        Config = (),
        Response = ServiceResponse,
        Error = actix_web::Error,
        InitError = (),
    >,
> {
    App::new()
        .route("/health", web::get().to(health))
        .route("/", web::get().to(greet))
        .route("/{name}", web::get().to(greet))
}
  • fn create_app() return an App that implements ServiceFactory trait.
  • we need to provide all the type params: Response, Config, Error, InitError.
pub trait ServiceFactory<Req> {
    /// Responses given by the created services.
    type Response;

    /// Errors produced by the created services.
    type Error;

    /// Service factory configuration.
    type Config;

    /// The kind of `Service` created by this factory.
    type Service: Service<Req, Response = Self::Response, Error = Self::Error>;

    /// Errors potentially raised while building a service.
    type InitError;

    /// The future of the `Service` instance.g
    type Future: Future<Output = Result<Self::Service, Self::InitError>>;

    /// Create and return a new service asynchronously.
    fn new_service(&self, cfg: Self::Config) -> Self::Future;
}

Where to put the tests?

Next to our code in an embedded test module:

#[cfg(test)]
mod tests {
    use super::*;
    // write tests here
}

In an external tests folder:

$ ls

src/
tests/
    health.rs

As part of public documentation (doc tests):

/// Check if a number is even.
/// ```rust
/// use zero2prod::is_even;
///
/// assert!(is_even(2));
/// assert!(!is_even(1));
/// ```
pub fn is_even(x: u64) -> bool {
x % 2 == 0 }

Execute with cargo test:

cargo test --test health

Test the /health endpoint with actix_web

// main.rs

async fn health(req: HttpRequest) -> impl Responder {
    HttpResponse::Ok().json(json!({ "status": "ok" }))
}

Write the first test using actix_web::test:

// tests.rs

#[cfg(test)]
use crate::health;
use crate::App;
use actix_web::{http::StatusCode, test, web};

#[actix_web::test]         // 👈 macro
async fn test_health_ok() {
    let app = test::init_service(
        create_app()       // 👈 the app logic
    ).await;
    let req = test::TestRequest::get().uri("/health").to_request();
    let resp = test::call_service(&app, req).await;
    assert_eq!(resp.status(), StatusCode::OK);

    // test the response body
    let body = response.text().await.expect("Failed to read response body");
    assert_eq!(body, r#"{"status":"ok"}"#);
}
  • We init the app by calling create_app() inside test::init_service().
  • Create a request by test::TestRequest::get().
  • Execute the request and get the response by test::call_service(&app, req).await.

Blackbox test the /health endpoint

Implement spawn_app()by using tokio::spawn()

fn spawn_app() {
    let server = newsletter::run().expect("Failed to run newsletter");
    // Start the server as background task
    let _ = tokio::spawn(server);
}

Write the test

#[tokio::test]               // 👈 macro
async fn health_test_ok() {
    spawn_app();

    let client = reqwest::Client::new();
    let response = client
        .get("http://127.0.0.1:8080/health")
        .send()
        .await
        .expect("Failed to send request");

    assert!(response.status().is_success());
    // assert_eq!(response., Some(2));
}

Use random port

  • Use TcpListener::bind("127.0.0.1:0") to listen on a random port
  • And return that port to be used in the tests
// Spawn the app on a random port
let listener = TcpListener::bind(addr).expect("random port");
let port = listener.local_addr().unwrap().port();

Notes

  • tokio::spawn shutdown all tasks spawned when it are dropped.
  • tokio::test spins up new runtime at the start of the test, and clean it up when the test ends

Author

I'm Oliver Nguyen. A software maker working mostly in Go and JavaScript. I enjoy learning and seeing a better version of myself each day. Occasionally spin off new open source projects. Share knowledge and thoughts during my journey. Connect with me on , , , and .

Back Back