mmag

ハマったことメモなど

actix-web + dieselの結合テストでトランザクションをコミットしない

やりたいこと

Diesel使ってDBに繋いでいるActix Webアプリのテストをするときに、ドキュメントのIntegration testsにはこんな例が書いてある。

#[actix_rt::test]
async fn test_index_post() {
    let mut app = test::init_service(App::new().route("/", web::get().to(index))).await;
    let req = test::TestRequest::post().uri("/").to_request();
    let resp = test::call_service(&mut app, req).await;
    assert!(resp.status().is_client_error());
}

main.rsに書いてあるのと同じようにApp::new()して、TestRequestを投げている。これを素直にやってしまうと開発用DBにどかどかレコードが作られたり更新されたりして都合が悪い。なのでテストのときはテスト用のDBに繋いで、さらにトランザクションをcommitしないでほしい。

やったこと

Dieselのドキュメントを眺めるとtest_transactionってのがあるんだけどちょっとやろうとしてるテストには向かなさそう。issueを漁ると何か発見。

github.com

CustomizeConnectionトレイトのon_acquire()begin_test_transaction()するとのこと。大体コピペだけどこんな風にした。わりと細かくモジュール切ってるのはconfigureで重複を避けるため。

// src/database.rs

use diesel::prelude::*;
use diesel::r2d2::{self, ConnectionManager, CustomizeConnection, Error};

pub type DatabasePool = r2d2::Pool<ConnectionManager<PgConnection>>;

#[derive(Debug)]
struct TestTransaction;

impl CustomizeConnection<PgConnection, Error> for TestTransaction {
    fn on_acquire(&self, conn: &mut PgConnection) -> std::result::Result<(), Error> {
        conn.begin_test_transaction().unwrap();
        Ok(())
    }
}

pub fn connection_pool() -> DatabasePool {
    let database_url = String::from("postgres://postgres:postgres@127.0.0.1:5432/my_app");
    let manager = ConnectionManager::<PgConnection>::new(database_url);

    r2d2::Pool::builder().build(manager).expect("Failed to create PostgreSQL pool")
}

pub fn test_connection_pool() -> DatabasePool {
    let database_url = String::from("postgres://postgres:postgres@127.0.0.1:5432/my_app_test");
    let manager = ConnectionManager::<PgConnection>::new(database_url);

    r2d2::Pool::builder()
        .connection_customizer(Box::new(TestTransaction))
        .build(manager)
        .expect("Failed to create PostgreSQL pool")
}
// src/app_config.rs

use crate::database;
use actix_web::web::ServiceConfig;

pub fn database(config: &mut ServiceConfig) {
    let pool = database::connection_pool();

    config.data(pool.clone());
}

pub fn test_database(config: &mut ServiceConfig) {
    let pool = database::test_connection_pool();

    config.data(pool.clone());
}

pub fn routes(config: &mut ServiceConfig) {
    config.service(...);
}
// tests/posts_test.rs

use actix_web::test::{init_service, read_response, TestRequest};
use actix_web::App;
use my_app::app_config::{routes, test_database};

#[actix_rt::test]
async fn test_track() {
    let mut app = init_service(App::new().configure(routes).configure(test_database)).await;

    let req = TestRequest::post().uri("/posts").to_request();

    ...
}

それっぽく動きそうではあるけど、このくらいのことってまあまあ普通のWebアプリケーションつくろうとしたら遭遇するだろうから、もうちょい色んなブログとか出てきそうなもんだけどそうでもなく。みんな違うフレームワークなりなんなり選んでるのかな。もっと面倒見てくれるやつ使えば?と言われたら、唇をかみしめて涙をこらえることしかできない。

Rust、かなり雰囲気でやっていて、まだまだフワッとしている。