Testing

Because if I have to write one more curl statement, I'm going to lose it.

It's important for any API to have a set of automated tests, that way you can change things with at least a little bit of confidence that the behavior will stay the same at the output. Thruster comes will a full test suite to help developer test their code. In order to use it, we'll have to do a little refactoring.

First, we're going to make a method specifically to create an app. Right now we're building the app and the server in the main function, so we'll split that out. First create a new file, app.rs

use sqlx::{Pool, Postgres};
use thruster::{
    context::typed_hyper_context::TypedHyperContext, m, middleware_fn, App, HyperRequest,
    MiddlewareNext, MiddlewareResult,
};

pub type Ctx = TypedHyperContext<RequestConfig>;

#[derive(Default)]
pub struct ServerConfig {}

#[derive(Default)]
pub struct RequestConfig {}

fn generate_context(request: HyperRequest, _state: &ServerConfig, _path: &str) -> Ctx {
    Ctx::new(request, RequestConfig {})
}

#[middleware_fn]
async fn hello(mut context: Ctx, _next: MiddlewareNext<Ctx>) -> MiddlewareResult<Ctx> {
    context.body("Hello, world!");

    Ok(context)
}

pub async fn app(
    _pool: Pool<Postgres>,
) -> Result<App<HyperRequest, Ctx, ServerConfig>, Box<dyn std::error::Error>> {
    Ok(
        App::<HyperRequest, Ctx, ServerConfig>::create(generate_context, ServerConfig {})
            .get("/hello", m![hello]),
    )
}

We basically copied over everything from main.rs and made Ctx, ServerConfig, and RequestConfig public. With that, we need to update our main.rs file as well, it'll get a lot simpler without all the app creation and middleware functions:

use log::info;
use sqlx::{postgres::PgPoolOptions, Executor};
use thruster::{hyper_server::HyperServer, ThrusterServer};

pub mod app;

#[tokio::main]
async fn main() {
    env_logger::init();
    info!("Starting server...");

    let pool = PgPoolOptions::new()
        .max_connections(5)
        .connect("postgres://postgres:password@localhost:5432/pigeon")
        .await
        .expect("Could not create postgres connection pool");

    pool.execute(include_str!("../schema.sql"))
        .await
        .expect("Could not create schema in database");

    let app = app::app(pool).await.expect("Could not create app");

    let server = HyperServer::new(app);
    server.build("0.0.0.0", 4321).await;
}

In order to use our library in tests we also need, well, a library! So create a lib.rs file. In that file, add a single line (for now.)

pub mod app;

Now we're in a great place to write a simple test, starting with our /hello route. Make a new file in a new folder named tests/ called hello.rs. I like to name tests based on routes, but you can really organize them whoever you please.

In hello.rs, we're going to make a very simple test as follows:

use carrier_pigeon::app::app;
use sqlx::{postgres::PgPoolOptions, Executor};
use thruster::Testable;

#[tokio::test]
async fn it_should_respond_to_create_user() {
    let pool = PgPoolOptions::new()
        .max_connections(5)
        .connect("postgres://postgres:password@localhost:5433/pigeon-test")
        .await
        .expect("Could not create postgres connection pool");

    pool.execute(include_str!("../schema.sql"))
        .await
        .expect("Could not create schema in database");
        
    // 1
    let app = app(pool).await.expect("Could not create app").commit();

    // 2
    let response = Testable::get(&app, "/hello", vec![])
        .await
        .expect("Could not run get")
        .expect_status(200, "It should return a 200 response code");

    assert_eq!(
        response.body_string(),
        "Hello, world!",
        "It should return the correct body"
    );
}

Some of this code should look very familiar! We're making a new database pool, and then running our quick schema to make sure it's in the correct state. One difference to note is that we're explicitly calling out port 5433 instead of the typical 5432. This is to make sure that we're not running tests on our local development database! We should start another container for testing before we actually run this,

docker run -d \
    -e POSTGRES_PASSWORD=password \
    -e POSTGRES_DB=pigeon-test \
    -p 5433:5432
    --name=pigeon-test \
    postgres

In the above rust code, I'll note a few important pieces here:

  1. Note that we're creating the app the same way we would in the main function, however we're also running this little commit method at the end. It's not terribly important for this tutorial as to why that needs to be run, but loosely it's a method that gets run when the server starts so that it can efficiently serve routes. If you didn't run it you would get 404 status codes. If you feel like it, go ahead and comment it out and see what part of the test fails.

  2. Thruster provides the Testable trait, which basically is an extension of App specifically for testing. It uses the same names as setting a new route for the app, so we use the "fully qualified syntax" so that the compiler knows we want to call the test version. This test version basically fakes a request using the same router and middleware, but ignoring the "Server" backend piece (the piece that thruster can easily swap out between hyper, actix, or the homegrown version for example.)

Now you should be ready to run the test! Run

cargo test

and bask in the glory of your newly tested code.

Last updated