Actix-web integration tests: reusing the main thread application

Viewed 156

I am using actix-web to write a small service. I'm adding integration tests to assess the functionality and have noticed that on every test I have to repeat the same definitions that in my main App except that it's wrapped by the test service:

let app = test::init_service(App::new().service(health_check)).await;

This can be easily extended if you have simple services but then when middleware and more configuration starts to be added tests start to get bulky, in addition it might be easy to miss something and not be assessing the same specs as the main App.

I've been trying to extract the App from the main thread to be able to reuse it my tests without success. Specifically what I'd like is to create a "factory" for the App:

pub fn get_app() -> App<????> {
App::new()
            .wrap(Logger::default())
            .wrap(IdentityService::new(policy))
            .service(health_check)
            .service(login)
}

So that I can write this in my tests

let app = get_app();
let service =  test::init_service(app).await;

But the compiler needs the specific return type which seems to be a chorizo composed of several traits and structs, some private.

Has anyone experience with this?

Thanks!

1 Answers

Define a declarative macro app! that builds the App, but define the routes using the procedural API, not the Actix build-in macros such as #[get("/")].

This example uses a database pool as a state - your application might have different kind of states or none at all.

#[macro_export]
macro_rules! app (
    ($pool: expr) => ({
        App::new()
            .wrap(middleware::Logger::default())
            .app_data(web::Data::new($pool.clone()))
            .route("/health", web::get().to(health_get))
            .service(web::resource("/items")
                .route(web::get().to(items_get))
                .route(web::post().to(items_post))
            )
    });
);

This can be used in the tests as:

#[cfg(test)]
mod tests {
    // more code here for get_test_pool
    #[test]
    async fn test_health() {
        let app = test::init_service(app!(get_test_pool().await)).await;

        let req = test::TestRequest::get().uri("/health").to_request();
        let resp = test::call_service(&app, req).await;
        assert!(resp.status().is_success());
    }
}

and in the main app as:

// More code here for get_main_pool
#[actix_web::main]
async fn main() -> Result<(),std::io::Error> {
    let pool = get_main_pool().await?;
    HttpServer::new(move || app!(pool))
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
}

In this context, get_main_pool must return, say, Result<sqlx::Pool<sqlx::Postgres>, std::io::Error> to be compatible with the signature requirements of actix_web::main. On the other hand, get_test_pool can simply return sqlx::Pool<sqlx::Postgres>.

Related