如何设计具备良好可测试性的Axum服务?
问题核心原因
你的代码有三个核心问题导致编译失败:
- 泛型辅助函数
helper<T>直接注册到路由时,Axum无法推断泛型T的具体类型,也没有对应约束满足tower的Service要求 - 你定义了用
Extension<T>提取业务handler,但路由构建时没有将handler实例注入到Extension层,即便编译通过运行时也无法正常获取 - 直接用泛型
T: Handler构建路由会导致每个不同的Handler实现都生成独立的路由类型,和BoxRoute要求的类型擦除规则不匹配
修正方案(对应Go接口式的易测试架构)
我们用trait对象动态分发实现和Go Interface完全一致的效果,业务逻辑完全和框架分离,测试时可以直接替换mock实现:
第一步:调整trait定义
// 加Send + Sync + 'static约束,满足异步多线程环境要求 trait Handler: Send + Sync + 'static { fn get(&self, get: GetRequest) -> Result<GetResponse, String>; }
第二步:调整路由构建和辅助函数
use std::sync::Arc; use axum::{Router, Json, Extension, routing::get, routing::BoxRoute}; // 路由构建函数接收擦除了具体类型的trait对象,所有Handler实现都通用 fn new_router(handler: Arc<dyn Handler>) -> Router<BoxRoute> { Router::new() .route("/", get(helper)) // 把业务handler注入到扩展层,方便辅助函数提取 .layer(Extension(handler)) .boxed() } // 辅助函数不再用泛型,直接提取trait对象,类型固定 async fn helper( Extension(handler): Extension<Arc<dyn Handler>>, Json(req): Json<GetRequest> ) -> Json<GetResponse> { Json(handler.get(req).unwrap()) }
第三步:main函数调用调整
#[tokio::main] async fn main() { // 把业务实现包成Arc<dyn Handler> let handler = Arc::new(Foo{}); let app = new_router(handler); axum::Server::bind(&"0.0.0.0:3000".parse().unwrap()) .serve(app.into_make_service()) .await .unwrap(); }
版本兼容说明
如果你升级到Axum 0.6及以上版本,BoxRoute已经废弃,不需要调用boxed方法,代码更简洁:
// Axum 0.6+ 版本的new_router写法 fn new_router(handler: Arc<dyn Handler>) -> Router { Router::new() .route("/", get(helper)) .layer(Extension(handler)) }
测试方式
需要mock时,只要定义新的结构体实现Handler trait即可,完全不需要修改框架层代码:
// 测试用mock实现 struct MockHandler; impl Handler for MockHandler { fn get(&self, req: GetRequest) -> Result<GetResponse, String> { Ok(GetResponse{ message: "mock response".to_owned() }) } } // 测试用例示例 #[cfg(test)] mod tests { use super::*; use axum::body::Body; use axum::http::{Request, StatusCode}; use tower::ServiceExt; #[tokio::test] async fn test_mock_handler() { let mock_handler = Arc::new(MockHandler); let app = new_router(mock_handler); let req = Request::builder() .uri("/") .method("GET") .header("content-type", "application/json") .body(Body::from("{}")) .unwrap(); let res = app.oneshot(req).await.unwrap(); assert_eq!(res.status(), StatusCode::OK); } }
架构最佳实践
- 业务逻辑完全封装在trait实现中,不需要依赖Axum任何组件,单独做单元测试时不需要启动Web服务
- 接口层(上述的helper函数)只做参数解析、错误转换、响应封装,不包含任何业务逻辑
- 所有外部依赖(数据库连接、第三方服务客户端等)都通过Extension注入,测试时可以随时替换为mock实现
内容的提问来源于stack exchange,提问作者Miss Yoimiya's puppy
相关产品推荐
相关产品推荐

