Skip to content

Testing Guide

How to run tests and write new tests for pgmt.

# Run all tests
cargo test
# Run with output
cargo test -- --nocapture
# Run specific test
cargo test test_table_creation
# Run by category
cargo test --test catalog # Catalog tests
cargo test --test migrations # Migration tests
cargo test --test cli # CLI tests

Test PostgreSQL introspection - verify we correctly read database structure.

An object kind is loaded by its raw fetch plus converter, which needs the shared catalog state (namespaces, types, extension ownership, comments) resolved on the same connection. load_converted supplies it, so a test doesn’t roll its own preamble.

Example:

use crate::helpers::raw::load_converted;
use pgmt::catalog::raw::table as raw_table;
#[tokio::test]
async fn test_fetch_tables() {
with_test_db(async |db| {
db.execute("CREATE TABLE users (id INT)").await;
let tables = load_converted(&mut *db.conn().await, raw_table::load).await.unwrap();
assert_eq!(tables.len(), 1);
}).await;
}

For anything that spans kinds — grants, dependencies between object types — load the whole catalog instead with Catalog::load_unfiltered(db.pool()).

Test end-to-end migration generation - verify schema changes produce correct SQL.

Example using 3-vector pattern:

#[tokio::test]
async fn test_table_migration() {
let helper = MigrationTestHelper::new().await;
helper.run_migration_test(
&[], // Both databases
&[], // Initial database only
&["CREATE TABLE users (id INT)"], // Target database only
|steps, _catalog| {
assert!(!steps.is_empty());
Ok(())
}
).await
}

Test command-line interface behavior.

Example:

#[tokio::test]
async fn test_apply_command() {
with_cli_helper(async |helper| {
helper.init_project()?;
helper.write_schema_file("users.sql", "CREATE TABLE users (id INT);")?;
helper.command()
.args(["apply"])
.assert()
.success();
Ok(())
}).await
}

Provides isolated test database with automatic cleanup:

use crate::helpers::harness::with_test_db;
#[tokio::test]
async fn test_something() {
with_test_db(async |db| {
// Use db.execute() and db.pool()
// Database automatically dropped when closure completes
}).await;
}

What it handles:

  • Creates unique temporary database for each test
  • Provides connection pool and SQL execution helpers
  • Guarantees cleanup even if test panics or fails

For multiple databases:

Tests comparing two database states (like migrations) can nest with_test_db calls:

#[tokio::test]
async fn test_with_multiple_databases() {
with_test_db(async |initial_db| {
with_test_db(async |target_db| {
initial_db.execute("CREATE TABLE users (id INT)").await;
target_db.execute("CREATE TABLE users (id INT, email TEXT)").await;
let initial = Catalog::load_unfiltered(initial_db.pool()).await.unwrap();
let target = Catalog::load_unfiltered(target_db.pool()).await.unwrap();
// Compare states...
}).await
}).await;
}

Simplifies migration testing with the 3-vector pattern:

let helper = MigrationTestHelper::new().await;
helper.run_migration_test(
&["CREATE TABLE shared (id INT)"], // SQL for both databases
&["CREATE TABLE only_initial (id INT)"], // SQL for initial database only
&["CREATE TABLE only_target (id INT)"], // SQL for target database only
|steps, final_catalog| {
// Verify migration steps and final state
Ok(())
}
).await

Provides temporary project for CLI testing:

use crate::helpers::cli::with_cli_helper;
#[tokio::test]
async fn test_cli() {
with_cli_helper(async |helper| {
helper.init_project()?;
helper.write_schema_file("file.sql", "CREATE TABLE ...")?;
helper.command().args(["apply"]).assert().success();
Ok(())
}).await
}

When adding new database object support, create tests for:

  1. Catalog fetch - Verify object is correctly read from PostgreSQL
  2. Comment support - Verify comments are fetched and managed
  3. Dependencies - Verify dependency tracking works
  4. Migration generation - Verify create/drop/modify operations
  5. Full pipeline - Test diff_all() → cascade::expand() → diff_order()

See the contributing guide for complete testing patterns.

Tests use Docker containers for PostgreSQL versions 13, 14, 15, 16, 17, and 18. Run ./scripts/test-setup.sh once to start all containers.

Test against a specific version:

DATABASE_URL=$(./scripts/test-db-url.sh 18) cargo test

Manual setup (if not using Docker):

createdb pgmt_test
export DATABASE_URL=postgres://localhost/pgmt_test
cargo test

Some PostgreSQL features require specific versions (e.g., security_invoker for views requires PostgreSQL 15+). Use the pg_major_version() helper to skip tests on older versions:

#[tokio::test]
async fn test_pg15_feature() -> Result<()> {
with_test_db(async |db| {
if pg_major_version(db.pool()).await? < 15 {
return Ok(()); // Skip on PostgreSQL < 15
}
// Test code that requires PG 15+ here
Ok(())
}).await
}

This pattern is used in tests/catalog/views.rs and tests/migrations/views.rs for features introduced after PostgreSQL 13.