6

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!

Ray
  • 96
  • 4
  • Sorry for being that "me too" guy, but I'm literally struggling with the exact same mental model problem. My first instinct was to extract the app creation into its own method, but that return type is just too complex. All the examples just do it within a callback for the Http Server. Pretty sure I'm missing something conceptually here, but coming from a PHP / Laravel background, having to maintain the app creation in 2 separate places just feels wrong in every possible way – Quasdunk Jun 13 '22 at 09:46
  • The solution I landed on was to just spin up the whole app including the HTTP server as it is (so not just the app part) and then just run requests against it from within the tests (e. g. with reqwest). Turned out not too bad – Quasdunk Jun 13 '22 at 12:27
  • @Quasdunk nice, that's what one guy on reddit suggested as well. Haven't continued with that project so far. Thanks for sharing! – Ray Jun 30 '22 at 19:26

2 Answers2

1

I was struggling with the same issue using actix-web@4, but I came up with a possible solution. It may not be ideal, but it works for my needs. I needed to bring in actix-service@2.0.2 and actix-http@3.2.2 in Cargo.toml as well.

I created a test.rs file with an initializer that I can use in all my tests. Here is what that file could look like for you:

use actix_web::{test::{self}, App, web, dev::{HttpServiceFactory, ServiceResponse}, Error};
use actix_service::Service;
use actix_http::{Request};

#[cfg(test)]
pub async fn init(service_factory: impl HttpServiceFactory + 'static) -> impl Service<Request, Response = ServiceResponse, Error = Error> {
    // connect to your database or other things to pass to AppState

    test::init_service(
        App::new()
            .app_data(web::Data::new(crate::AppState { db }))
            .service(service_factory)
    ).await
}

I use this in my API services to reduce boilerplate in my integration tests. Here is an example:

// ...

#[get("/")]
async fn get_index() -> impl Responder {
    HttpResponse::Ok().body("Hello, world!")
}

#[cfg(test)]
mod tests {
    use actix_web::{test::TestRequest};

    use super::{get_index};

    #[actix_web::test]
    async fn test_get_index() {
        let mut app = crate::test::init(get_index).await;

        let resp = TestRequest::get().uri("/").send_request(&mut app).await;
        assert!(resp.status().is_success(), "Something went wrong");
    }
}

I believe the issue you ran into is trying to create a factory for App (which is a bit of an anti-pattern in Actix) instead of init_service. If you want to create a function that returns App I believe the preferred convention is to use configure instead. See this issue for reference: https://github.com/actix/actix-web/issues/2039.

Dylan M
  • 21
  • 5
  • Have you thought about a way to set the init fn as a static ref that way it only runs once, if so would you know a solution? – wubalubadub Apr 26 '23 at 09:33
0

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>.