Actix-web集成测试:重用主线程应用程序

7

我正在使用actix-web编写一个小型服务。我正在添加集成测试以评估其功能,并注意到在每个测试中,我都必须重复主应用程序中的相同定义,只是它被测试服务包装:

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

如果您只有简单的服务,那么这可以很容易地扩展,但是当中间件和更多配置开始添加时,测试开始变得笨重,另外可能很容易错过某些内容,并且不能评估与主应用程序相同的规格。

我一直在尝试将应用程序从主线程中提取出来,以便能够在我的测试中重复使用它,但没有成功。具体而言,我想创建一个“工厂”来创建应用程序:

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

这样我就可以在我的测试代码中编写这段内容。

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

但编译器需要具体的返回类型,这个类型仿佛是由几个特性和结构体组成的腊肠,其中一些是私有的。

有人有相关经验吗?

谢谢!


很抱歉成为那个“我也是”的人,但我真的在与完全相同的心理模型问题作斗争。我的第一反应是将应用程序创建提取到自己的方法中,但返回类型太复杂了。所有的例子都是在 Http 服务器的回调函数中完成的。我很确定我在概念上漏掉了什么,但从 PHP / Laravel 的背景出发,在两个不同的地方维护应用程序创建感觉就是完全错误的。 - Quasdunk
我采用的解决方案是将整个应用程序(包括HTTP服务器)一起启动,然后从测试中运行请求(例如使用reqwest)。结果还不错。 - Quasdunk
@Quasdunk 很好,这也是 Reddit 上一个人建议的。到目前为止我还没有继续那个项目。感谢分享! - Ray
2个回答

2

我在使用actix-web@4时遇到了同样的问题,但是我想出了一个可能的解决方案。这可能不是最理想的解决方法,但对我的需求来说有效。我还需要在Cargo.toml中引入actix-service@2.0.2actix-http@3.2.2

我创建了一个test.rs文件,其中包含一个初始化器,我可以在所有的测试中使用。以下是你可能需要的文件示例:

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
}

我在我的API服务中使用它来减少集成测试中的样板代码。以下是一个示例:

// ...

#[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");
    }
}

我认为你遇到的问题是试图为 App 创建一个工厂(这在 Actix 中有点反模式),而不是使用 init_service。如果你想创建一个返回 App 的函数,我认为首选的约定是使用 configure。请参考此问题:https://github.com/actix/actix-web/issues/2039

你有没有考虑过将 init 函数设置为静态引用,这样它只会运行一次,如果是这样的话,你知道解决方案吗? - wubalubadub

0

定义一个声明性宏app!,用于构建App,但使用过程式API定义路由,而不是Actix内置的宏,例如#[get("/")]

此示例将数据库池用作状态 - 您的应用程序可能具有不同类型的状态或根本没有状态。

#[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))
            )
    });
);

这可以在测试中使用:

#[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());
    }
}

并在主应用程序中:

// 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
}

在这种情况下,get_main_pool 必须返回类似于 Result<sqlx::Pool<sqlx::Postgres>, std::io::Error> 的内容,以符合 actix_web::main 的签名要求。另一方面,get_test_pool 可以简单地返回 sqlx::Pool<sqlx::Postgres>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接