ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

使用 [sqlx::test] 为 Axum + SQLx API 编写集成测试:axum-social-with-tests 实战剖析

使用 [sqlx::test] 为 Axum + SQLx API 编写集成测试:axum-social-with-tests 实战剖析 数据库后端【免费下载链接】sqlx The Rust SQL Toolkit. An async, pure Rust SQL crate featuring compile-time checked queries without a DSL. Supports PostgreSQL, MySQL, and SQLite.项目地址https://gitcode.com/gh_mirrors/sql/sqlx点击查看免费下载导读本文以 SQLx 仓库中的 axum-social-with-tests 示例 为完整骨架系统讲解如何用 SQLx 的过程宏#[sqlx::test]为一个基于 Axum 与 PostgreSQL 的社交 API用户、帖子、评论编写真正的集成测试。读完本文你将掌握#[sqlx::test]的自动测试数据库创建、迁移自动执行、fixtures 种子数据注入与测试后自动清理机制学会用tower::ServiceExt::oneshot免启动端口直接测试 Router并理解该测试方案在 SQLx 底层sqlx-core/src/testing/mod.rs是如何实现的。示例项目定位一个带测试的 Axum 社交 API这个示例展示的核心命题是如何为一个使用 Axum SQLx 构建的 API 编写集成测试并且测试里直接使用真实数据库而不是 mock。项目的全部代码位于 examples/postgres/axum-social-with-tests/包含三个核心模块用户模块注册POST /v1/user与登录校验逻辑帖子模块发布POST /v1/post与按时间倒序浏览GET /v1/post评论模块发表评论POST /v1/post/{postId}/comment与按时间正序查看评论GET /v1/post/{postId}/comment。项目的tests/目录下共有三个集成测试文件user.rs、post.rs、comment.rs覆盖了每个端点的快乐路径与若干失败路径是学习 SQLx 官方推荐测试写法的第一手素材。⚠️ 官方警告为了保持示例简洁该项目省略了大量关键的安全防护措施如邮箱/手机验证、速率限制、会话管理等。它可以作为学习起点但直接部署到生产环境风险自负。这一警告写在 README.md 中源码里 user.rs 的注释也再次强调该注册接口没有正常注册流程应有的任何校验。项目结构与依赖一览examples/postgres/axum-social-with-tests/ ├── migrations/ # 三个版本化迁移文件1_user / 2_post / 3_comment ├── src/ │ ├── main.rs # 入口连接数据库、跑迁移、启动服务 │ ├── lib.rs # 库 crate供集成测试直接复用 app() │ ├── password.rs # Argon2 密码哈希/校验spawn_blocking │ └── http/ # Axum Router 与各 handler、错误类型 └── tests/ ├── common.rs # 测试公共工具JSON 请求构造、响应解析、断言辅助 ├── fixtures/ # 种子数据 SQLusers / posts / comments ├── user.rs ├── post.rs └── comment.rs关键依赖配置见 Cargo.toml[dependencies] axum { version 0.8, features [macros] } sqlx { path ../../.., features [runtime-tokio, tls-rustls-ring, postgres, time, uuid] } tokio { version 1.25.0, features [rt-multi-thread, macros] } argon2 0.6.0-rc.8 validator { version 0.20.0, features [derive] } time 0.3.48 uuid { version 1.12.1, features [serde] } serde_with { version 3.18.0, features [time_0_3] } [dev-dependencies] http-body-util 0.1 serde_json 1.0.142 tower 0.5.2其中有两点值得注意sqlx通过path ../../..直接引用仓库根目录的 SQLx 源码并启用了postgres、time、uuid、runtime-tokio、tls-rustls-ring特性测试所需的tower提供ServiceExt::oneshot、http-body-util提供BodyExt读取响应体、serde_json全部放在[dev-dependencies]不进入生产依赖。应用层骨架库 crate 与 Router 的拆分一个值得复用的设计模式是把构建 Router 的逻辑放进lib.rs而不是main.rs。这样集成测试tests/ 目录下的文件会作为独立 crate 编译可以直接use sqlx_example_postgres_axum_social::http拿到app()函数。src/lib.rs 只做两件事导出http模块、私有化password模块。src/http/mod.rs 是 Router 的组装点pub fn app(db: PgPool) - Router { Router::new() .merge(user::router()) .merge(post::router()) .with_state(db) } pub async fn serve(db: PgPool) - anyhow::Result() { let listener tokio::net::TcpListener::bind(0.0.0.0:8080).await?; axum::serve(listener, app(db)).await?; Ok(()) }src/main.rs 负责启动从DATABASE_URL环境变量读取连接串用dotenvy::var读取.env、通过PgPoolOptions::new().max_connections(20)建立连接池、执行sqlx::migrate!().run(db)应用迁移最后调用http::serve(db)监听0.0.0.0:8080。数据模型与迁移设计三个迁移文件1_user.sql、2_post.sql、3_comment.sql构成了完整的社交数据模型-- 1_user.sql create table user ( user_id uuid primary key default gen_random_uuid(), username text unique not null, password_hash text not null ); -- 2_post.sql create table post ( post_id uuid primary key default gen_random_uuid(), user_id uuid not null references user(user_id), content text not null, created_at timestamptz not null default now() ); create index on post(created_at desc); -- 3_comment.sql create table comment ( comment_id uuid primary key default gen_random_uuid(), post_id uuid not null references post(post_id), user_id uuid not null references user(user_id), content text not null, created_at timestamptz not null default now() ); create index on comment(post_id, created_at);要点user_id、post_id、comment_id均使用gen_random_uuid()生成默认值PostgreSQL 13 内置函数username带unique约束这是测试中用户名已被占用 → 409 Conflict断言的数据库级依据created_at默认now()且帖子在created_at desc上建索引、评论在(post_id, created_at)上建索引对应 handler 里的排序查询。从源码结构看迁移目录按1_user、2_post、3_comment的序号排列与sqlx::migrate!()的默认版本号解析约定一致测试框架会按序自动执行这些迁移。#[sqlx::test] 集成测试机制从宏到底层实现这是本示例的灵魂。#[sqlx::test]是 SQLx 提供的过程宏其完整实现位于 sqlx-macros-core/src/test_attr.rs运行时支撑位于 sqlx-core/src/testing/mod.rs。宏展开把测试函数包装为 TestFntest_attr.rs 中宏最终展开为调用::sqlx::testing::TestFn::run_test(f, args)。它会根据测试函数的签名接收PgPool、PoolConnection、PgPoolOptions ConnectOptions或空参生成对应的TestFn实现并携带TestArgs——其中包含test_path测试文件路径migrator可选即测试所需的迁移集合示例中自动采用sqlx::migrate!()fixtures可选测试要注入的种子 SQL 集合。运行流程创建数据库 → 跑迁移 → 注入 fixtures → 跑测试 → 清理底层执行函数 run_test 与 setup_test_db 揭示了完整的生命周期连接模板数据库通过DB::test_context(args)连接 setup 数据库通常由环境变量DATABASE_URL指定的那个实例创建独立测试数据库setup_test_db为每个测试函数创建一个全新的、随机命名的数据库保证测试之间数据完全隔离执行迁移若指定了 migrator则调用migrator.run_direct(None, mut conn, false)在新数据库上按序应用全部迁移注入 fixtures遍历args.fixtures逐条执行每个 fixture 的 SQL 内容setup_test_db运行测试test_fn(test_context.pool_opts, test_context.connect_opts)建立连接池并调用你写的测试函数体清理测试成功返回后调用DB::cleanup_test(...)删除该测试数据库同时 run_test_with_pool 会以 10 秒超时关闭连接池若测试函数结束后仍未释放 Pool会打印警告test {test_path} held onto Pool after exiting。这意味着你不需要在测试里手动CREATE DATABASE、执行迁移或清空表——#[sqlx::test]全部包办测试函数拿到的PgPool已经指向一个全新、已迁移、可选带种子数据的数据库。测试公共设施common.rs 里的四个利器tests/common.rs 被三个测试文件通过mod common;共享文件顶部#![allow(dead_code)]避免未被使用告警它封装了集成测试最繁琐的部分1.RequestBuilderExttrait——把 JSON 变成 HTTP 请求pub trait RequestBuilderExt { fn json(self, json: serde_json::Value) - RequestBody; fn empty_body(self) - RequestBody; } impl RequestBuilderExt for request::Builder { fn json(self, json: serde_json::Value) - RequestBody { self.header(Content-Type, application/json) .body(Body::from(json.to_string())) .expect(failed to build request) } // ... }配合tower::ServiceExt::oneshot测试可以直接Request::post(/v1/user).json(json!({...}))完全不需要起一个真实的 TCP 端口。2.response_json——读取并解析响应体pub async fn response_json(resp: mut Response) - serde_json::Value { assert_eq!(resp.headers().get(CONTENT_TYPE).expect(expected Content-Type), application/json); let bytes resp.collect().await.expect(error reading response body).to_bytes(); serde_json::from_slice(bytes).expect(failed to read response body as json) }它同时断言了Content-Type: application/json并利用http_body_util::BodyExt::collect把响应体聚合成字节后反序列化。3.expect_uuid/expect_rfc3339_timestamp——强类型断言expect_uuid断言 JSON 值可解析为Uuidexpect_rfc3339_timestamp用OffsetDateTime::parse(s, Rfc3339)断言时间戳符合 RFC 3339 格式。两个函数都标注了#[track_caller]断言失败时错误信息会精确指向测试源码中的调用行便于定位。测试用例逐个拆解user.rs注册端点的四种场景test_create_user 是唯一一个不带 fixtures的测试因为它要验证注册新用户本身#[sqlx::test] async fn test_create_user(db: PgPool) { let mut app http::app(db); // Happy path! let resp1 app.borrow_mut() .oneshot(Request::post(/v1/user).json(json! {{ username: alice, password: rustacean since 2015 }})) .await.unwrap(); assert_eq!(resp1.status(), StatusCode::NO_CONTENT); // ... 更多场景 }它依次覆盖场景请求期望状态码期望响应体快乐路径alice / 合法密码204 NO_CONTENT—用户名已占用再次用 alice409 CONFLICT{message: username taken}用户名非法含空格的用户名422 UNPROCESSABLE_ENTITY校验错误errors.username为数组密码非法空密码422 UNPROCESSABLE_ENTITY校验错误errors.password为数组其中用户名已占用的 409 判定来自 user.rs 对数据库约束错误的专门映射当sqlx::Error::Database的constraint()等于user_username_key时转换为Error::Conflict(username taken)——这是利用数据库唯一约束反向驱动 API 语义的典型写法。用户名/密码的合法性规则在 user.rs 用validator声明式定义用户名长度 3–16 且匹配^[0-9A-Za-z_]$密码长度 8–32。post.rs创作与列表test_create_post使用#[sqlx::test(fixtures(users))]——先注入一个用户alice再以 alice 的身份发帖。断言包括快乐路径返回200 OK响应体中的username alice、content回显一致且postId可解析为 UUID、createdAt是合法 RFC 3339 时间戳用户名错误aliceee或密码错误rustaceansince2015都返回422 UNPROCESSABLE_ENTITY和invalid username/password。test_list_posts使用#[sqlx::test(fixtures(users, posts))]注入两条帖子alice 与 bob 各一条然后断言GET /v1/post返回数组长度为 2、按created_at倒序排列assert!(created_at_0 created_at_1, posts must be sorted in descending order)。发帖 handler 在 src/http/post/mod.rs 中用了 CTEwith inserted_post as (insert ... returning ...)inner join user一条 SQL 完成插入并带出用户名的复合查询这展示了sqlx::query_as!对复杂查询的静态类型检查能力。comment.rs评论的创建与列表test_create_comment用fixtures(users, posts)准备数据然后向/v1/post/d9ca2672-24c5-4442-b32f-cd717adffbaa/comment发帖评论路径中的 UUID 正是 posts.sql 里固定写入的 alice 的帖子 ID。这展示了 fixtures 的一个重要能力通过固定 UUID 让测试路径参数与种子数据强绑定。test_list_comments用fixtures(users, posts, comments)注入 3 条评论验证对 d9ca…alice 的帖子返回 2 条评论按created_at正序assert!(created_at_0 created_at_1)对 7e3d…bob 的帖子返回 1 条评论。fixtures可读、可预测的种子数据fixtures 放在 tests/fixtures/ 下文件名与#[sqlx::test(fixtures(users, posts, comments))]中声明的名称一一对应即 SQLx 会去查找tests/fixtures/users.sql等文件。其内容特点是表名显式带上 schema 前缀public.user且所有主键/外键 UUID 均为硬编码字面量SQL 注释中标注了用户名: alice; 密码: rustacean since 2015与测试断言中的登录凭据一一对应password_hash字段直接存放预先算好的 Argon2id 哈希串避免在测试中执行慢速哈希。例如 users.sqlINSERT INTO public.user (user_id, username, password_hash) VALUES (51b374f1-93ae-4c5c-89dd-611bda8412ce, alice, $argon2id$v19$m4096,t3,p1$3v3ats/tYTXAYs3q9RycDw$ZltwjS3oQwPuNmL9f6DNbsH5N81dTVZhVNbUQzmmVU), (c994b839-84f4-4509-ad49-59119133d6f5, bob, $argon2id$v19$m4096,t3,p1$1zbkRinUH9WHzkyu8C1Vlg$70pu5Cca/s3d0nh5BYQGkN7s9cqlNxTE7rFZaUaP4c);密码哈希/校验的运行时实现见 password.rs使用argon2::Argon2::default()哈希与PasswordHash::newverify_password校验且由于 Argon2 是 CPU 密集操作两个函数都通过tokio::task::spawn_blocking移出异步运行时避免阻塞事件循环。运行测试的完整指引前置条件一个可连接的 PostgreSQL 实例以及一个有权限创建/删除数据库的超级用户连接串设置环境变量DATABASE_URL示例依赖它连接测试所需的 setup 数据库。执行命令在仓库根目录下运行DATABASE_URLpostgres://postgres:passwordlocalhost/postgres cargo test -p sqlx-example-postgres-axum-social#[sqlx::test]会为tests/目录下的每个测试函数自动创建独立测试数据库、执行 migrations、按需注入 fixtures测试结束后自动删除数据库因此多次运行之间不会残留数据测试具备可重复性。关于并行与隔离由于每个测试函数拥有完全独立的数据库cargo test的默认并行执行是安全的SQLx 内部通过DB::cleanup_test(DB::db_name(args))见 sqlx-core/src/testing/mod.rs在成功路径下回收测试数据库保证测试运行的整洁。总结这套测试方案的三个可迁移经验把 Router 构建放在 lib cratesrc/lib.rs src/http/mod.rs集成测试才能用http::app(db)免端口直测让#[sqlx::test]接管数据库生命周期自动建库、自动迁移、自动注入 fixtures、自动清理测试函数只关心业务断言用固定 UUID 的 fixtures 与common.rs的断言工具response_json/expect_uuid/expect_rfc3339_timestamp让测试既贴近真实 HTTP 语义JSON 序列化/反序列化又具备强类型的可读性。最后重申示例作者的提醒这个项目是一个教学起点省略了大量生产级安全措施直接用于生产环境风险自负。若要在生产场景复用应补充邮件/手机验证、速率限制、会话管理、请求体大小限制等防护并参考 src/http/user.rs 中的注释逐项补齐。赞分享数据库后端【免费下载链接】sqlx The Rust SQL Toolkit. An async, pure Rust SQL crate featuring compile-time checked queries without a DSL. Supports PostgreSQL, MySQL, and SQLite.项目地址https://gitcode.com/gh_mirrors/sql/sqlx点击查看免费下载相关推荐告别繁琐配置axumPostgreSQLSQLx无缝集成实战告别繁琐配置axumPostgreSQLSQLx无缝集成实战 你是否还在为Rust Web开发中的数据库集成烦恼配置复杂、依赖冲突、连接池管理混乱本文后端Web框架Realworld Axum SQLx 项目教程Realworld Axum SQLx 项目教程 1. 项目的目录结构及介绍 realworld axum sqlx/ ├── Cargo.toml ├── s终极Rust Web开发指南使用realworld-axum-sqlx构建高性能后端终极Rust Web开发指南使用realworld axum sqlx构建高性能后端 realworld axum sqlx是一个基于Rust语言的Realw上一篇Wand-Enhancer终极指南三步免费解锁WeMod Pro完整功能下一篇Wand-Enhancer三步免费解锁WeMod Pro完整功能的终极指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表