Search code examples
rustintegration-testingactix-web

Cannot pass app_data content to handler methods in Actix Web integration tests


I'm want to extract my AppState struct from HttpRequest in my handler function. It works when I'm calling that handler through my main app instance, but doesn't work inside integration tests.

Handler function:

pub async fn handle(req: HttpRequest, user: Json<NewUser>) -> Result<HttpResponse, ShopError> {
    let state = req.app_data::<Data<AppState>>().expect("app_data is empty!");
    Ok(HttpResponse::Ok().finish())
}

main.rs:

#[actix_web::main]
pub async fn main() -> std::io::Result<()> {
    dotenv().ok();
    let chat_server = Lobby::default().start();
    let state = AppState {
            static_data: String::from("my_data")
        };
    HttpServer::new(move || {
        App::new()
            .app_data(Data::new(state.clone()))
            .service(web::scope("/").configure(routes::router))
            .app_data(Data::new(chat_server.clone()))
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

Test:

#[actix_web::test]
    async fn sanity_test() {
        let app = test::init_service(App::new().route("/", web::post().to(handle))).await;
        let user = NewUser {
            username: String::from("legit_user"),
            password: String::from("123"),
        };
        let state = AppState {
            static_data: String::from("my_data")
        };
        let req = test::TestRequest::post()
            .app_data(Data::new(state))
            .set_json(&user)
            .uri("/")
            .to_request();
        let resp = test::call_service(&app, req).await;
        assert!(resp.status().is_success());
    }

Test output:

running 1 test
thread 'routes::login::tests::sanity_test' panicked at 'app_data is empty!', src/routes/login.rs:35:50

For some reason it is always None. I've tried using Data<AppState> extractor but then whole handler is not even called, again, only when testing, otherwise everything works.

Cargo.toml:

[dependencies]
diesel = { version = "1.4.4", features = ["postgres", "r2d2", "chrono"] }
diesel_migrations = "1.4.0"
chrono = { version = "0.4.19", features = ["serde"] }
dotenv = "0.15.0"
actix = "0.13.0"
actix-web = "4.1.0"
actix-web-actors = "4.1.0"
bcrypt = "0.13.0"
uuid = { version = "1.1.2", features = ["serde", "v4"] }
serde_json = "1.0.82"
serde = { version = "1.0.139", features = ["derive"] }
validator = { version = "0.16.0", features = ["derive"] }
derive_more = "0.99.17"
r2d2 = "0.8.10"
lazy_static = "1.4.0"
jsonwebtoken = "8.1.1"

I'm aware of app_data is always None in request handler thread, and it does not solve my problem since for me everything works except when testing.


Solution

  • From what I see in your integration test, app_data is not configured for the app instance passed to test::init_service, which is why the handler panics. When the app is configured with app_data as you have done it in main, app_data becomes available for the handler.

    With the code below, the handler can access AppData in the integration test. The main difference from the original post is that the app in the integration test is configured with app_data, not the request.

    use actix_web::{
        HttpServer, 
        HttpRequest, 
        HttpResponse, 
        App,
        web::{Data, Json},
        post,
    };
    
    use serde::{Deserialize, Serialize};
    
    #[derive(Deserialize, Serialize)]
    pub struct NewUser{
        username: String,
        password: String,
    }
    
    #[derive(Clone)]
    struct AppState{
        static_data: String,
    }
    
    #[post("/")]
    pub async fn handle(
        req: HttpRequest, 
        _user: Json<NewUser>
    ) -> HttpResponse {
        let state = req
            .app_data::<Data<AppState>>()
            .expect("app_data is empty!");
        println!("Static data: {}", state.static_data);
        HttpResponse::Ok().finish()
    }
    
    #[actix_web::main]
    async fn main() -> std::io::Result<()> {
        let state = AppState{
            static_data: "Sparta".to_string(),
        };
        HttpServer::new(move || {
            App::new()
                .app_data(Data::new(state.clone()))
                .service(handle)
        })
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
        use actix_web::{
            test,
        };
    
        #[actix_web::test]
        async fn itest() {
            // Set up app with test::init_service
            let state = AppState {
                static_data: "Sparta".to_string(),
            };
    
            let app = test::init_service(
                App::new()
                    .app_data(Data::new(state.clone()))
                    .service(handle)
            ).await;
    
            // Prepare request
            let sample_user = NewUser{
                username: "legit_user".to_string(),
                password: "nosecret123".to_string(),
            };
    
            let req = test::TestRequest::post()
                
                .set_json(&sample_user)
                .uri("/")
                .to_request();
            
            // Send request and await response
            let resp = test::call_service(&app, req).await;
            assert!(resp.status().is_success())
    
        }
    }