Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Actix-web integration tests: reusing the main thread application

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!

like image 818
Ray Avatar asked May 25 '26 18:05

Ray


2 Answers

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 [email protected] and [email protected] 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.

like image 168
Dylan M Avatar answered May 27 '26 07:05

Dylan M


If anyone else has encountered the issue Ray discussed some time ago, you'll know it can be quite challenging. After spending some time struggling with this situation and experimenting with different approaches, such as passing a mutable reference to a function or trying to figure out the correct return value for each case, I finally found a solution that perfectly fits my use case. I hope it will be beneficial for you as well: consider creating a macro.

In my scenario, the macro looks something like this:

#[macro_export]
macro_rules! setup_test_app {
  () => {{
    use actix_web::{App, test};
    use crate_name::setup_cors;
    use crate_name::general_config;

    let app = test::init_service(
      App::new()
        .configure(general_config) 
    ).await;

    app
  }};
}

To use it in your tests, you can do something similar to the following:

#[actix_rt::test]
async fn test_app_initialization() {
  let mut app = setup_test_app!();
  // Your test code here...
}

This approach simplifies the setup for each test by encapsulating the initialization logic within a macro, making your test code cleaner and more maintainable.

like image 45
Diego Flórez Avatar answered May 27 '26 07:05

Diego Flórez



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!