Rust | SQLx の macro `sqlx::test` でテストを実装する
SQLx とは
SQLx は Rust のデーベース接続用のクレートです。
SQLx の詳細や特徴については、いくつか記事を公開しているので割愛します。
参考までに...👨💻👨💻👨💻
- Rust | SQLx の macro
query!
とquery_as!
を使いこなす - Rust | SQLx の macro
query_file!
とquery_file_as!
で複雑な SQL クエリを分離する - Rust | SQLx で transaction & commit / rollbackを実装する
sqlx::test
について
テスト用のDBを作成してくれる
sqlx::test
では、テスト実行時にテスト用のデータベースを作成し、
そのコネクションプールを引数として受け取ることができます🙆♂️
また、テスト終了時には作成されたデータベースを削除してくれます🙆♂️🙆♂️
テストしたゴミデータが残らない、とても便利な仕様と言えるでしょう。
なお、環境変数に DATABASE_URL
を設定する必要があります。
以下のデータベースに対応しています。
データベース |
DATABASE_URL の有無 |
---|---|
Postgres | 必要 |
MySQL | 必要 |
SQLite | 不要[1] |
fixtures 機能
fixtures でテスト実行時にあらかじめデータベースにレコードをインサートするためのクエリを実行することができます。
テストファイルと同じディレクトリに ./fixtures/users.sql
のようにファイルを格納することで、fixtures を活用できます。
use sqlx::PgPool;
#[sqlx::test(fixtures("users", "posts"))]
async fn test_create_comment(pool: PgPool) -> sqlx::Result<()> {
// Test
Ok(())
}
sqlx::test
でテストを実装する
以前実装した SQLx を使ったトランザクション実装のソースコード のテストを実装してみます。
Cargo.tml
[dependencies]
dotenvy = "0.15.7"
sqlx = { version = "0.8.1", features = ["postgres", "runtime-tokio", "chrono"] }
tokio = { version = "1.40.0", features = ["full"] }
main.rs
関数 create_order_slip
のテストを test_create_order_slip
として、
関数 create_order_slip_details
のテストを test_create_order_slip_details
として実装しています。
(トランザクションの関係で test_create_order_slip_details
の中でも create_order_slip
を実行してはいますが...)
use sqlx::postgres::PgPoolOptions;
use sqlx::types::chrono::NaiveDateTime;
use sqlx::{PgPool, Postgres, Transaction};
use std::env;
#[tokio::main]
async fn main() {
// 環境変数を読み取り、URLを生成
dotenvy::dotenv().expect("Failed to read .env file");
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
// DBコネクションを生成
let db = PgPoolOptions::new()
.max_connections(10)
.connect(&database_url)
.await
.unwrap_or_else(|_| panic!("Cannot connect to the database"));
// トランザクションを生成する
let mut tx = db.begin().await.expect("transaction error.");
// 注文伝票データを作成する
match create_order_slip(&mut tx, "注文書").await {
Ok(os) => {
// 明細行データを生成
let details = vec![
NewOrderSlipDetail {
order_slip_id: os.id,
product_name: "えんぴつ".to_string(),
product_count: 10,
},
NewOrderSlipDetail {
order_slip_id: os.id,
product_name: "消しゴム".to_string(),
product_count: 3,
},
NewOrderSlipDetail {
order_slip_id: os.id,
product_name: "ボールペン".to_string(),
product_count: 5,
},
];
// 注文伝票に紐づく明細行データを作成する
match create_order_slip_details(&mut tx, details).await {
Ok(_) => println!("done."),
Err(e) => println!("{:?}", e),
}
}
Err(e) => println!("{:?}", e),
}
// コミット
// ここで rollback() を指定することも可能
let _ = tx.commit().await.unwrap_or_else(|e| {
println!("{:?}", e);
});
}
#[derive(Debug)]
struct OrderSlip {
id: i32,
title: String,
created_at: NaiveDateTime,
updated_at: NaiveDateTime,
}
#[derive(Debug)]
struct OrderSlipDetail {
id: i32,
order_slip_id: i32,
product_name: String,
product_count: i32,
created_at: NaiveDateTime,
updated_at: NaiveDateTime,
}
struct NewOrderSlipDetail {
order_slip_id: i32,
product_name: String,
product_count: i32,
}
async fn create_order_slip(
tx: &mut Transaction<'_, Postgres>,
title: &str,
) -> Result<OrderSlip, sqlx::Error> {
sqlx::query_as!(
OrderSlip,
r#"
insert into
order_slips (title)
values
($1)
returning *
"#,
title.to_string()
)
.fetch_one(&mut **tx)
.await
}
async fn create_order_slip_details(
tx: &mut Transaction<'_, Postgres>,
details: Vec<NewOrderSlipDetail>,
) -> Result<(), sqlx::Error> {
for d in details {
let _ = sqlx::query!(
r#"
insert into
order_slip_details (order_slip_id, product_name, product_count)
values
($1, $2, $3)
"#,
d.order_slip_id,
d.product_name,
d.product_count
)
.execute(&mut **tx)
.await?;
}
Ok(())
}
#[sqlx::test()]
async fn test_create_order_slip(db: PgPool) {
let mut tx = db.begin().await.expect("transaction error.");
let created = create_order_slip(&mut tx, "[TEST] 注文書").await;
println!("{:?}", created);
let _ = tx.commit().await.unwrap_or_else(|e| {
println!("{:?}", e);
});
assert_eq!(created.is_ok(), true);
}
#[sqlx::test()]
async fn test_create_order_slip_details(db: PgPool) {
let mut tx = db.begin().await.expect("transaction error.");
match create_order_slip(&mut tx, "[TEST] 注文書").await {
Ok(os) => {
// 明細行データを生成
let details = vec![
NewOrderSlipDetail {
order_slip_id: os.id,
product_name: "マッキー".to_string(),
product_count: 10,
},
NewOrderSlipDetail {
order_slip_id: os.id,
product_name: "色鉛筆".to_string(),
product_count: 3,
},
NewOrderSlipDetail {
order_slip_id: os.id,
product_name: "ノートブック".to_string(),
product_count: 5,
},
];
// 注文伝票に紐づく明細行データを作成する
match create_order_slip_details(&mut tx, details).await {
Ok(_) => println!("done."),
Err(e) => println!("{:?}", e),
}
}
Err(e) => println!("{:?}", e),
}
let _ = tx.commit().await.unwrap_or_else(|e| {
println!("{:?}", e);
});
assert_eq!(true, true);
}
テストを実行してみます。
実行結果は以下です。
% cargo test -- --nocapture
running 2 tests
Ok(OrderSlip { id: 1, title: "[TEST] 注文書", created_at: 2024-09-01T11:53:35.916197, updated_at: 2024-09-01T11:53:35.916197 })
done.
test test_create_order_slip ... ok
test test_create_order_slip_details ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.61s
ID が 1 で、タイトルが [TEST] 注文書
であることが分かります。
また、何度実行しても同じ結果となることから、テスト時にデータベースが作成されており、ゴミデータが残っていないことも分かります!
fixtures を試す
fixtures を試すために、関数 get_order_slips
とそのテストを追加で実装しました。
async fn get_order_slips(db: &PgPool) -> Vec<OrderSlip> {
let selected = sqlx::query_as!(
OrderSlip,
r#"
select
*
from
order_slips
"#,
)
.fetch_all(db)
.await;
selected.unwrap_or_else(|_| Vec::new())
}
#[sqlx::test(fixtures("order_slips"))]
async fn test_get_order_slips(db: PgPool) {
let got = get_order_slips(&db).await;
println!("{:?}", got);
assert_eq!(got[0].title == "[fixtures] 注文書", true);
}
そして、fixtures フォルダを以下の構成で作成します。
src
├── fixtures
│ └── order_slips.sql
└── main.rs
insert into order_slips (title)
values
('[fixtures] 注文書');
テストの実行結果です。
% cargo test test_get_order_slips -- --nocapture
running 1 test
[OrderSlip { id: 1, title: "[fixtures] 注文書", created_at: 2024-09-01T11:54:14.750440, updated_at: 2024-09-01T11:54:14.750440 }]
test test_get_order_slips ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 1.26s
order_slips.sql
でインサートしたレコードが取得されていることが分かります!
ちなみに、#[sqlx::test()]
の場合はレコードが存在しないため、パニックが発生します。
これは期待通りの結果ですね。
fixtures の配置について
今回は src 直下に fixtures を作成しましたが、
SQLx のサンプルコードを見ていると tests フォルダ内のテストで使用されていたため tests の直下に fixtures が作成されていました。
まとめ
データベースまわりのテストもしっかりと実装していきましょう🔥🔥
Discussion