3.7 KiB
+++ title = "[TRIAGE] Integration test isolation strategy — per-test temp DB vs shared DB with transaction rollback?" priority = 8 status = "done" ticket_type = "task" dependencies = [] +++
This is a triage decision ticket. It must be resolved before dependent implementation tickets can proceed. Integration test isolation strategy: should each test get its own temporary database (per-test DB creation/deletion) or should tests share a database and use transaction rollback for cleanup? 1. **Per-test temp DB** — each test creates a fresh SQLite file (or in-memory DB) and drops it on cleanup. Maximum isolation, slower due to migration overhead per test. 2. **Shared DB with transaction rollback** — all tests share one DB, each test wraps its operations in a transaction that is rolled back at the end. Faster, but requires the test harness to manage transactions. 3. **Per-test in-memory SQLite** — SQLite `:memory:` database per test. Fast (no file I/O) and fully isolated. May require `--test-threads=1` if the server shares state. **Per-test temp SQLite file** (Option 1 variant), using the `tempfile` crate for RAII cleanup.Rationale:
-
Transaction rollback (Option 2) is not viable for HTTP integration tests. The test harness spawns a real Axum server as a tokio task. That server manages its own SQLx connection pool and commits transactions independently. The test client cannot intercept or roll back those server-side transactions.
-
In-memory SQLite (Option 3) conflicts with SQLx connection pools. Each connection in a SQLx pool that opens
sqlite::memory:gets its own isolated empty database. The test server and the test client would be talking to different databases. Named shared-cache URIs (file:test_N?mode=memory&cache=shared) work around this but are less-known, have edge cases in SQLx, and offer no meaningful speed advantage over a temp file on a tmpfs. -
Per-test temp file (chosen) works correctly with SQLx pools because all pool connections share the same file path. Migration runs once per test (≈ milliseconds for 2 tables).
TempDirfrom thetempfilecrate provides RAII cleanup — the file is deleted when theTestContextis dropped. Tests run in parallel safely (each has a unique path).
Implementation in test harness (tests/helpers.rs):
pub struct TestContext {
_db_dir: TempDir, // keeps temp file alive; deleted on drop
pub base_url: String,
_shutdown: tokio::task::JoinHandle<()>,
}
pub async fn spawn_test_server() -> TestContext {
let db_dir = TempDir::new().unwrap();
let db_path = db_dir.path().join("test.sqlite");
let pool = SqlitePool::connect(&format!("sqlite:{}", db_path.display()))
.await
.unwrap();
sqlx::migrate!("./migrations").run(&pool).await.unwrap();
let app = build_router(Arc::new(NativeRepository::new(pool)));
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
let handle = tokio::spawn(axum::serve(listener, app).into_future());
TestContext {
_db_dir: db_dir,
base_url: format!("http://127.0.0.1:{port}"),
_shutdown: handle,
}
}
Dev-dependency added: tempfile = "3" (tracked in ticket 5f5ba0).