Logicky Blog

Logickyの開発ブログです

Rust(Rocket)でsqlxを使って並列的にDBの統合テストをする

先日この投稿を書いたのですが、Rocket と sqlx でクリーンアーキテクチャっぽくしたものの、トランザクションをうまく使えなくて、都度 truncate を実行する直列的なテストをしていましたが、超簡単にトランザクションが使えることにやっと気づいたので、並列的にテストができるようになりました。

前回トランザクションが使えなかった理由

  • repository の関数の引数で、通常の DB 接続とトランザクションを両方共うまく受け付けることができなかった。
  • 受け付けるだけならできるのですが、ジェネリクスを使うことになり、その場合、automock を使う部分や、依存性注入の部分で、どうも不都合が生じて、あきらめた。

今回トランザクションが使えるようになった理由

  • repository の関数の引数で、通常の DB 接続とトランザクションを両方共うまく受け付ける方法が分かった。(ジェネリクスを使わない方法)

通常の DB 接続とトランザクションを両方共うまく受け付ける方法

repository 関数のコード例

async fn create(&self, con: &mut PgConnection, project: &Project) -> Result<Project, AppError> {
    query_as!(
        Project,
        "INSERT INTO projects (user_id, name, image_url) VALUES ($1, $2, $3) RETURNING *",
        project.user_id,
        project.name,
        project.image_url
    )
    .fetch_one(&mut *con)
    .await
    .app_error(500, "Failed to create project")
}

統合テストのコード例

#[cfg(test)]
mod tests {
    use crate::models::project_model::Project;
    use crate::repositories::postgres::project_repo::{ProjectRepo, ProjectRepoImpl};
    use crate::test::db::create_db_con_for_test;
    use crate::test::repositories::prepare::{project::create_project, user::create_user};
    use sqlx::Connection;

    #[tokio::test]
    async fn test_create_project() {
        let mut db_con = create_db_con_for_test().await.unwrap();
        let mut tx = db_con.begin().await.unwrap();
        let user = create_user(&mut tx, None).await.unwrap();
        let result = create_project(&mut tx, &user.id).await;
        assert!(result.is_ok());
        tx.rollback().await.unwrap();
    }
}
use crate::models::project_model::Project;
use crate::repositories::postgres::project_repo::{ProjectRepo, ProjectRepoImpl};
use crate::services::error_handling::AppError;
use sqlx::postgres::PgConnection;
use uuid::Uuid;

pub async fn create_project(
    db_con: &mut PgConnection,
    user_id: &Uuid,
) -> Result<Project, AppError> {
    let project_repo = ProjectRepoImpl::new();
    let name = "新しいプロジェクト";
    let project = Project::new(None, *user_id, name, Some("Test URL".to_string()));
    project_repo.create(&mut *db_con, &project).await
}
#[cfg(test)]
use crate::db::{postgres::DbCon, redis::RedisCon};
#[cfg(test)]
use dotenv::dotenv;
#[cfg(test)]
use sqlx::{postgres::PgPoolOptions, Error};
#[cfg(test)]
use std::env;

#[cfg(test)]
pub async fn create_db_con_for_test() -> Result<DbCon, Error> {
    dotenv().ok();
    let db_url = env::var("DATABASE_URL_TEST").expect("DATABASE_URL_TEST must be set");
    let db_pool = PgPoolOptions::new()
        .max_connections(1)
        .connect(&db_url)
        .await?;
    db_pool.acquire().await
}

今後やってみたいこと(上記とあんまり関係ないものも含む)

  • 何故PgConnectionを使うと問題ないのか調べる
  • #[cfg(test)]の使い方、もっと調べる
  • エラーハンドリングのやり方研究する
    • repository 関数では sqlx のエラーをそのまま全部返して、use_case 側か controller 側で StatusCode つきのエラーに変換させるイメージ