http - 如何使用 Rust/Axum 设计测试友好的应用程序?
问题描述
当我尝试使用Rust/Axum构建应用程序时,我未能将Axum框架与我自己的处理程序分开。使用Golang,经典的方法是定义一个Interface
,实现它并将处理程序注册到框架。这样,很容易提供模拟handler
和测试。但是,我无法使用Axum。我在上面定义了 a trait
,但它无法编译。
此处列出了一个演示:
use std::net::ToSocketAddrs;
use std::sync::{Arc, Mutex};
use serde_derive::{Serialize, Deserialize};
use serde_json::json;
use axum::{Server, Router, Json};
use axum::extract::Extension;
use axum::routing::BoxRoute;
use axum::handler::get;
#[tokio::main]
async fn main() {
let app = new_router(
Foo{}
);
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
trait Handler {
fn get(&self, get: GetRequest) -> Result<GetResponse, String>;
}
struct Foo {}
impl Handler for Foo {
fn get(&self, req: GetRequest) -> Result<GetResponse, String> {
Ok(GetResponse{ message: "It works.".to_owned()})
}
}
fn new_router<T:Handler>(handler: T) -> Router<BoxRoute> {
Router::new()
.route("/", get(helper))
.boxed()
}
fn helper<T:Handler>(
Extension(mut handler): Extension<T>,
Json(req): Json<GetRequest>
) -> Json<GetResponse> {
Json(handler.get(req).unwrap())
}
#[derive(Debug, Serialize, Deserialize)]
struct GetRequest {
// omited
}
#[derive(Debug, Serialize, Deserialize)]
struct GetResponse {
message: String
// omited
}
并且这里列出了Rust报告的错误。
error[E0599]: the method `boxed` exists for struct `Router<axum::routing::Layered<Trace<axum::routing::Layered<AddExtension<Nested<Router<BoxRoute>, Route<axum::handler::OnMethod<fn() -> impl Future {direct}, _, (), EmptyRouter>, EmptyRouter<_>>>, T>>, SharedClassifier<ServerErrorsAsFailures>>>>`, but its trait bounds were not satisfied
--> src/router.rs:25:10
|
25 | .boxed()
| ^^^^^ method cannot be called on `Router<axum::routing::Layered<Trace<axum::routing::Layered<AddExtension<Nested<Router<BoxRoute>, Route<axum::handler::OnMethod<fn() -> impl Future {direct}, _, (), EmptyRouter>, EmptyRouter<_>>>, T>>, SharedClassifier<ServerErrorsAsFailures>>>>` due to unsatisfied trait bounds
|
::: /Users/lebrancebw/.cargo/registry/src/github.com-1ecc6299db9ec823/axum-0.2.5/src/routing/mod.rs:876:1
|
876 | pub struct Layered<S> {
| --------------------- doesn't satisfy `<_ as tower_service::Service<Request<_>>>::Error = _`
|
= note: the following trait bounds were not satisfied:
`<axum::routing::Layered<Trace<axum::routing::Layered<AddExtension<Nested<Router<BoxRoute>, Route<axum::handler::OnMethod<fn() -> impl Future {direct}, _, (), EmptyRouter>, EmptyRouter<_>>>, T>>, SharedClassifier<ServerErrorsAsFailures>>> as tower_service::Service<Request<_>>>::Error = _`
error: aborting due to previous error; 13 warnings emitted
For more information about this error, try `rustc --explain E0599`.
error: could not compile `short_url`
To learn more, run the command again with --verbose.
我想关键是我的设计不土气。Axum 项目有什么典型的布局吗?或一些有用的建议?
解决方案
问题是你想测试什么。我会假设你有一些核心逻辑和一个 HTTP 层。您要确保:
- 请求被正确路由;
- 请求被正确解析;
- 使用预期参数调用核心逻辑;
- 并且来自核心的返回值被正确格式化为 HTTP 响应。
要对其进行测试,您需要生成一个模拟了核心逻辑的服务器实例。@lukemathwalker 在他的博客和“Rust 中的零生产”一书中很好地描述了如何通过实际的 TCP 端口生成用于测试的应用程序。它是为 Actix-Web 编写的,但这个想法也适用于 Axum。
您不应该使用axum::Server::bind
,而应该使用axum::Server::from_tcp
它来传递它,std::net::TcpListner
它允许您使用 `TcpListener::bind("127.0.0.1:0") 在任何可用端口上生成测试服务器。
为了使核心逻辑可注入(和可模拟),我将其声明为结构并在其上实现所有业务方法。像这样:
pub struct Core {
public_url: Url,
secret_key: String,
storage: Storage,
client: SomeThirdPartyClient,
}
impl Core {
pub async fn create_something(
&self,
new_something: NewSomething,
) -> Result<Something, BusinessErr> {
...
}
使用所有这些部分,您可以编写一个函数来启动服务器:
pub async fn run(listener: TcpListener, core: Core)
这个函数应该封装路由配置、服务器日志配置等。
可以使用扩展层机制将核心提供给处理程序,如下所示:
...
let shared_core = Arc::new(core);
...
let app = Router::new()
.route("/something", post(post_something))
...
.layer(AddExtensionLayer::new(shared_core));
可以使用扩展提取器在参数列表中声明处理程序中的哪个:
async fn post_something(
Extension(core): Extension<Arc<Core>>,
Json(new_something): Json<NewSomething>,
) -> impl IntoResponse {
core
.create_something(new_something)
.await
}
Axum 示例包含一个关于错误处理和依赖注入的示例。你可以在这里查看。
最后但同样重要的是,现在您可以使用类似的库来模拟 Core mockall
,编写spawn_app
返回运行服务器的主机和端口的函数,针对它运行一些请求并进行断言。
来自 Let's Get Rusty 频道的 Bogdan的视频为 mockall 提供了一个良好的开端。
如果您觉得答案中缺少某些内容,我将很乐意提供更多详细信息。
推荐阅读
- java - Fire-base 卡住并且无法与 android studio 连接
- sql - Hana sql 如果左侧包含空且右侧包含值,则将单元格在 sql 中向左移动
- python - 如何为图形添加键?
- javascript - jQuery 调用 .GET() 两次并获得结果
- c - scanf 未正确读取 int
- python - 按值将多维数组拆分为另一个
- r - 在 ggplot2 中等效于地图的 eqscplot (geom_sf & coord_sf)
- javascript - 如何以正确的方式进行代码拆分?
- python - WeasyPrint HTML到图像转换:如何使图像大小适应内容?
- c - Scanf 更改 C 中的输入