🐼

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

2023/11/27に公開

先日この投稿を書いたのですが、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つきのエラーに変換させるイメージ

Discussion