Resource Acquisition Is Initialization (RAII) test clusters

Version 0.1.0 Updated Nov 08, 2025

pg_embedded_setup_unpriv::TestCluster wraps bootstrap_for_tests() with a Resource Acquisition Is Initialization (RAII) lifecycle. Constructing the guard starts PostgreSQL using the discovered settings, applies the environment produced by the bootstrap helper, and exposes the configuration to callers. Dropping the guard stops the instance and restores the prior process environment, so subsequent tests start from a clean slate.

use pg_embedded_setup_unpriv::{TestCluster, error::BootstrapResult};

fn exercise_cluster() -> BootstrapResult<()> {
    let cluster = TestCluster::new()?;
    let url = cluster.settings().url("app_db");
    // Issue queries using any preferred client here.
    drop(cluster); // PostgreSQL shuts down automatically.
    Ok(())
}

The guard keeps PGPASSFILE, TZ, TZDIR, and the XDG directories populated for the duration of its lifetime, making synchronous tests usable without extra setup. Unit and behavioural tests assert that postmaster.pid disappears after drop, demonstrating that no orphaned processes remain.

Using the `rstest` fixture

pg_embedded_setup_unpriv::test_support::test_cluster exposes an rstest fixture that constructs the RAII guard on demand. Import the fixture so it is in scope and declare a test_cluster: TestCluster parameter inside an #[rstest] function; the macro injects the running cluster automatically.

use pg_embedded_setup_unpriv::{test_support::test_cluster, TestCluster};
use rstest::rstest;

#[rstest]
fn runs_migrations(test_cluster: TestCluster) {
    let metadata = test_cluster.connection().metadata();
    assert!(metadata.port() > 0);
}

The fixture integrates with rstest-bdd v0.1.0-alpha4 so behaviour tests can remain declarative as well:

use pg_embedded_setup_unpriv::{test_support::test_cluster, TestCluster};
use rstest_bdd_macros::scenario;

#[scenario(path = "tests/features/test_cluster_fixture.feature", index = 0)]
fn coverage(test_cluster: TestCluster) {
    let _ = test_cluster.environment();
}

If PostgreSQL cannot start, the fixture panics with a SKIP-TEST-CLUSTER-prefixed message that retains the original error. Unit tests fail immediately, while behaviour tests can convert known transient conditions into soft skips via the shared skip_message helper.

Connection helpers and Diesel integration

TestCluster::connection() exposes TestClusterConnection, a lightweight view over the running cluster's connection metadata. Use it to read the host, port, superuser name, generated password, or the .pgpass path without cloning the entire bootstrap struct. When you need to persist those values beyond the guard you can call metadata() to obtain an owned ConnectionMetadata.

Enable the diesel-support feature to call diesel_connection() and obtain a ready-to-use diesel::PgConnection. The default feature set keeps Diesel optional for consumers, while make test already enables --all-features so the helper is exercised by the smoke tests.

use diesel::prelude::*;
use pg_embedded_setup_unpriv::TestCluster;

# fn main() -> pg_embedded_setup_unpriv::BootstrapResult<()> {
let cluster = TestCluster::new()?;
let connection = cluster.connection();
let url = connection.database_url("postgres");
assert!(url.starts_with("postgresql://"));

#[cfg(feature = "diesel-support")]
{
    let mut diesel_conn = connection.diesel_connection("postgres")?;
    #[derive(QueryableByName)]
    struct ValueRow {
        #[diesel(sql_type = diesel::sql_types::Integer)]
        value: i32,
    }

    let rows: Vec<ValueRow> = diesel::sql_query("SELECT 1 AS value")
        .load(&mut diesel_conn)?;
    assert_eq!(rows[0].value, 1);
}
# Ok(())
# }