Set RUST_LOG=pg_embed::observability=info to emit tracing spans that describe
privilege drops, directory ownership or permission updates, scoped environment
application, and the postgresql_embedded setup/start/stop lifecycle. The log
target keeps sensitive values redacted: environment changes are rendered as
KEY=set or KEY=unset, and PostgreSQL settings avoid echoing passwords.
Enable RUST_LOG=pg_embed::observability=debug to surface a sanitized snapshot
of the prepared settings, including the version requirement, host and port,
installation and data directories, and the .pgpass location. Passwords log as
<redacted> and configuration entries are reduced to their keys, so secrets
stay out of the debug stream, even when bootstrap fails early. Subscribers that
record span enter/exit events, for example via FmtSpan::ENTER|CLOSE, can
reconstruct the lifecycle flow without needing additional instrumentation in
downstream crates.
Environment change summaries are truncated once they exceed roughly 512
characters, while the change count is always recorded. Lifecycle failures now
emit at error level, so log streams can distinguish genuine errors from the
normal informational lifecycle noise.
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. The
test_cluster and shared_test_cluster fixtures are synchronous and
constructed via TestCluster::new(), so they must not be used in async tests;
TestCluster::start_async() should be used for async tests.
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, a Behaviour-Driven Development (BDD)
crate, 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.
When to use each fixture:
| Fixture | Use case |
|---|---|
test_cluster |
Tests that modify cluster-level settings or state |
shared_test_cluster |
Tests that only need database-level isolation |
Table 2: Fixture selection for pg-embed-setup-unpriv test clusters.
The shared cluster is particularly effective when combined with template databases (see "Database lifecycle management" below) to reduce per-test overhead from seconds to milliseconds.
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.
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(())
# }
Database lifecycle management
TestClusterConnection provides methods for programmatically creating and
dropping databases on the running cluster. These are useful for test isolation
patterns where each test creates its own database to avoid cross-test
interference.
use pg_embedded_setup_unpriv::TestCluster;
# fn main() -> pg_embedded_setup_unpriv::BootstrapResult<()> {
let cluster = TestCluster::new()?;
let conn = cluster.connection();
// Create a new database
conn.create_database("my_test_db")?;
// Check if a database exists
assert!(conn.database_exists("my_test_db")?);
assert!(conn.database_exists("postgres")?); // Built-in database
// Drop the database when done
conn.drop_database("my_test_db")?;
assert!(!conn.database_exists("my_test_db")?);
# Ok(())
# }
The TestCluster type also exposes convenience wrappers that delegate to the
connection methods:
use pg_embedded_setup_unpriv::TestCluster;
# fn main() -> pg_embedded_setup_unpriv::BootstrapResult<()> {
let cluster = TestCluster::new()?;
// These delegate to cluster.connection().create_database(...) etc.
cluster.create_database("my_test_db")?;
assert!(cluster.database_exists("my_test_db")?);
cluster.drop_database("my_test_db")?;
# Ok(())
# }
All methods connect to the postgres database as the superuser to execute the
Data Definition Language (DDL) statements. Errors are returned when:
- Creating a database that already exists
- Dropping a database that does not exist
- Dropping a database with active connections
- Connection to the cluster fails
Template databases for fast test isolation
PostgreSQL's CREATE DATABASE … TEMPLATE mechanism clones an existing database
via a filesystem-level copy, completing in milliseconds regardless of schema
complexity. This is significantly faster than running migrations on each test
database.
use pg_embedded_setup_unpriv::TestCluster;
# fn main() -> pg_embedded_setup_unpriv::BootstrapResult<()> {
let cluster = TestCluster::new()?;
// Create a template database and apply migrations
cluster.create_database("my_template")?;
// ... run migrations on my_template ...
// Clone the template for each test (milliseconds vs seconds)
cluster.create_database_from_template("test_db_1", "my_template")?;
cluster.create_database_from_template("test_db_2", "my_template")?;
# Ok(())
# }
Template helpers live on TestClusterConnection and are also exposed on
TestCluster for convenience. Use unique database names (for example,
format!("test_{}", uuid::Uuid::new_v4())) to avoid collisions under parallel
execution.
The ensure_template_exists method provides concurrency-safe template creation
with per-template locking to prevent race conditions when multiple tests try to
initialize the same template simultaneously. If the setup callback fails or
panics after creating the template, the helper drops that partially created
template before returning the error or resuming the panic:
use pg_embedded_setup_unpriv::TestCluster;
# fn main() -> pg_embedded_setup_unpriv::BootstrapResult<()> {
let cluster = TestCluster::new()?;
// Only creates and migrates if the template doesn't exist
cluster.ensure_template_exists("migrated_template", |db_name| {
// Run migrations on the newly created database
// e.g., diesel::migration::run(&mut conn)?;
Ok(())
})?;
// Clone for the test
cluster.create_database_from_template("test_db", "migrated_template")?;
# Ok(())
# }
For versioned template names that automatically invalidate when migrations
change, use the hash_directory helper to generate a content-based hash:
use pg_embedded_setup_unpriv::test_support::hash_directory;
# fn main() -> pg_embedded_setup_unpriv::BootstrapResult<()> {
let hash = hash_directory("migrations")?;
let template_name = format!("template_{}", &hash[..8]);
// Template name changes when any migration file changes
# Ok(())
# }
If you already track a migration version, include it in the template name
instead (for example, format!("template_v{SCHEMA_VERSION}")). This keeps
template invalidation explicit without hashing the migration directory.
Performance comparison
The following table reports approximate benchmark results from one development test environment. Treat the timings as environment-dependent guidance rather than guaranteed performance figures.
Table 3: Performance comparison for test isolation approaches.
| Approach | Bootstrap | Per-test overhead | Isolation |
|---|---|---|---|
Per-test TestCluster |
Per test | 20–30 seconds | Full |
| Shared cluster, fresh database | Once | 1–5 seconds | Database |
| Shared cluster, template clone | Once | 10–50 ms | Database |
When to use each approach:
- Per-test cluster (
test_clusterfixture): Use when tests modify cluster-level settings, require specific PostgreSQL versions, or need complete isolation from other tests. - Shared cluster with fresh databases: Use when tests need database-level isolation but can share the same cluster. Suitable when migration overhead is acceptable.
- Shared cluster with template cloning (
shared_test_clusterfixture): Use for maximum performance when tests only need database-level isolation. Requires upfront template creation, but reduces per-test overhead by orders of magnitude.
Database cleanup strategies
When using a shared cluster, databases created during tests persist until explicitly dropped or the cluster shuts down. Consider these strategies:
Explicit cleanup: Drop databases after each test to reclaim disk space and prevent name collisions:
use pg_embedded_setup_unpriv::TestCluster;
# fn main() -> pg_embedded_setup_unpriv::BootstrapResult<()> {
let cluster = TestCluster::new()?;
let db_name = format!("test_{}", uuid::Uuid::new_v4());
cluster.create_database_from_template(&db_name, "my_template")?;
// ... run test ...
cluster.drop_database(&db_name)?; // Explicit cleanup
# Ok(())
# }
Cluster teardown cleanup: Let the shared cluster drop all databases when the test binary exits. This is simpler but uses more disk space during the test run:
use pg_embedded_setup_unpriv::test_support::shared_cluster;
# fn main() -> pg_embedded_setup_unpriv::BootstrapResult<()> {
let cluster = shared_cluster()?;
let db_name = format!("test_{}", uuid::Uuid::new_v4());
cluster.create_database_from_template(&db_name, "my_template")?;
// ... run test ...
// Database dropped automatically when cluster shuts down
# Ok(())
# }
Active connection handling: Dropping a database with active connections
fails. Ensure all connections are closed before calling drop_database. If
using connection pools, drain the pool first.
Automatic cleanup with TemporaryDatabase
The TemporaryDatabase guard provides RAII cleanup semantics. When the guard
goes out of scope, the database is automatically dropped:
use pg_embedded_setup_unpriv::TestCluster;
# fn main() -> pg_embedded_setup_unpriv::BootstrapResult<()> {
let cluster = TestCluster::new()?;
// Create a temporary database with automatic cleanup
let temp_db = cluster.temporary_database("my_test_db")?;
// Use the database
let url = temp_db.url();
// ... run queries ...
// Database is dropped automatically when temp_db goes out of scope
drop(temp_db);
# Ok(())
# }
For template-based workflows, use temporary_database_from_template:
use pg_embedded_setup_unpriv::TestCluster;
# fn main() -> pg_embedded_setup_unpriv::BootstrapResult<()> {
let cluster = TestCluster::new()?;
// Ensure the template exists
cluster.ensure_template_exists("migrated_template", |_| Ok(()))?;
// Create a temporary database from the template
let temp_db = cluster.temporary_database_from_template("test_db", "migrated_template")?;
// Database is automatically dropped when temp_db goes out of scope
# Ok(())
# }
Drop behaviour:
drop_database()— Explicitly drop the database, failing if connections exist. Consumes the guard.force_drop()— Terminate active connections before dropping. Useful when connection pools haven't been drained.- Implicit drop (guard goes out of scope) — Best-effort drop with a warning logged on failure.