testdb

package module
v0.0.0-...-c12ca72 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 2, 2025 License: MIT Imports: 11 Imported by: 0

README

testdb

True database isolation for Go tests using PostgreSQL's CREATE DATABASE. No Docker required.

Go Reference

Features

  • True Isolation - Each test gets its own database
  • Parallel Tests - Run tests concurrently with t.Parallel()
  • Zero Docker - Uses native PostgreSQL CREATE DATABASE
  • Auto Cleanup - Databases are dropped automatically
  • Migration Support - Works with Tern, Goose, and golang-migrate
  • PostgreSQL Support - Production-ready with pgxpool

Quick Start

Prerequisites

A running PostgreSQL server (local installation, managed instance, or remote). testdb connects to PostgreSQL - it doesn't manage the server itself.

Install
go get github.com/bashhack/testdb/postgres
Basic Usage
package myapp_test

import (
    "context"
    "testing"

    "github.com/bashhack/testdb"
    "github.com/bashhack/testdb/postgres"
)

func TestUsers(t *testing.T) {
    // Create isolated test database with your migrations
    pool := postgres.Setup(t,
        testdb.WithMigrations("./migrations"),
        testdb.WithMigrationTool(testdb.MigrationToolTern),
    )

    // Use the database - tables from migrations are ready
    _, err := pool.Exec(context.Background(),
        "INSERT INTO users (email) VALUES ($1)", "test@example.com")
    if err != nil {
        t.Fatalf("Insert failed: %v", err)
    }

    // Cleanup is automatic via t.Cleanup()
}

That's it! Each test gets an isolated database with your schema ready.

Why testdb?

testdb uses PostgreSQL's native CREATE DATABASE to give each test its own isolated database.

Complete isolation - Each test gets an actual database, not a transaction or schema. Test transactions, DDL, concurrent operations - anything your application does in production.

Simple setup - Works with your existing PostgreSQL server. No containers to orchestrate, no Docker daemon required.

True parallelism - Run hundreds of tests concurrently with t.Parallel(). Each test has its own database, eliminating coordination complexity.

Standard tooling - Integrates with existing migration tools (Tern, Goose, golang-migrate). Use the migrations you already have.

Shared database approach:

func TestUsers(t *testing.T) {
    // Share a database with all other tests
    db := getSharedTestDB()

    // Manually clean up between tests
    defer truncateTables(db, "users", "orders")

    // Can't use t.Parallel() safely
    // State from other tests can leak
}

testdb approach:

func TestUsers(t *testing.T) {
    t.Parallel() // Safe - each test has its own database

    pool := postgres.Setup(t,
        testdb.WithMigrations("./migrations"),
        testdb.WithMigrationTool(testdb.MigrationToolTern),
    )
    // Complete isolation - no state leakage possible
}

Configuration

Environment Variables

Set TEST_DATABASE_URL or DATABASE_URL:

export TEST_DATABASE_URL="postgres://user:pass@localhost:5432/postgres"

Or use the default: postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable

Options
pool := postgres.Setup(t,
    testdb.WithMigrations("./migrations"),
    testdb.WithMigrationTool(testdb.MigrationToolTern),
    testdb.WithAdminDSN("postgres://custom:5432/postgres"),
    testdb.WithDBPrefix("myapp_test"),
)

Available options:

  • WithMigrations(dir) - Set migration directory (requires WithMigrationTool)
  • WithMigrationTool(tool) - Use tern, goose, or migrate (requires WithMigrations)
  • WithAdminDSN(dsn) - Override admin connection string
  • WithMigrationToolPath(path) - Path to migration binary
  • WithDBPrefix(prefix) - Database name prefix (default: "test")
  • WithVerbose() - Enable verbose logging for debugging

Advanced Usage

Built-in Initializers

testdb provides two built-in initializers for PostgreSQL:

PoolInitializer (Default)

Creates *pgxpool.Pool - recommended for most PostgreSQL applications. Provides full access to PostgreSQL-specific features:

// postgres.Setup() uses PoolInitializer automatically
pool := postgres.Setup(t)

// Or explicitly with postgres.New()
db := postgres.New(t, &postgres.PoolInitializer{})
pool := db.Entity().(*pgxpool.Pool)

// Full pgx capabilities: arrays, JSON, COPY, LISTEN/NOTIFY
pool.QueryRow(ctx, "SELECT ARRAY[1,2,3]").Scan(&arr)
SqlDbInitializer

Creates *sql.DB - use when your application code or dependencies expect database/sql interfaces:

db := postgres.New(t, &postgres.SqlDbInitializer{})
sqlDB := db.Entity().(*sql.DB)

// Standard database/sql operations
sqlDB.QueryRow("SELECT * FROM users WHERE id = $1", 1).Scan(&name)

// Works with libraries that expect *sql.DB
repo := myorm.NewRepository(sqlDB)

SqlDbInitializer uses pgx/v5/stdlib under the hood, so you get pgx's PostgreSQL support with database/sql compatibility.

When to use SqlDbInitializer:

  • Your application uses *sql.DB, *sql.Tx, or *sql.Rows
  • You're working with ORMs or libraries that expect *sql.DB
  • You need database/sql semantics (standard connection pooling, transactions)

When to use PoolInitializer:

  • You're using pgx directly (recommended for new PostgreSQL projects)
  • You need PostgreSQL-specific features (arrays, JSON types, COPY, LISTEN/NOTIFY)
  • You want the best performance and feature set
Custom Initializer

If you need custom database initialization (e.g., using GORM, sqlx):

type MyInitializer struct{}

func (m *MyInitializer) InitializeTestDatabase(ctx context.Context, dsn string) (interface{}, error) {
    // Your custom initialization
    return myapp.InitDB(dsn)
}

func TestAdvanced(t *testing.T) {
    db := postgres.New(t, &MyInitializer{},
        testdb.WithMigrations("./migrations"),
        testdb.WithMigrationTool(testdb.MigrationToolTern),
    )
    myDB := db.Entity().(*myapp.DB)
    // Use your custom DB type
}
Using Just the DSN

If you want full control over connections without an initializer:

provider := &postgres.PostgresProvider{}
db, err := testdb.New(t, provider, nil,
    testdb.WithMigrations("./migrations"),
    testdb.WithMigrationTool(testdb.MigrationToolTern),
)
if err != nil {
    t.Fatalf("Failed to create database: %v", err)
}

// Run migrations manually if needed
if err := db.RunMigrations(); err != nil {
    t.Fatalf("Migrations failed: %v", err)
}

// Use the DSN to create your own connection
myCustomPool := myapp.ConnectDB(db.DSN())
defer myCustomPool.Close()

// Don't forget cleanup
defer db.Close()
Helper Function Pattern
func setupDB(t *testing.T) *pgxpool.Pool {
    t.Helper()
    return postgres.Setup(t,
        testdb.WithMigrations("./migrations"),
        testdb.WithMigrationTool(testdb.MigrationToolTern),
    )
}

func TestSomething(t *testing.T) {
    pool := setupDB(t)
    // Use pool...
}
Testing Without Migrations

For demonstrating isolation mechanics or simple tests:

func TestIsolation(t *testing.T) {
    // Create database without migrations
    pool := postgres.Setup(t)

    // Use for simple queries
    var result int
    pool.QueryRow(context.Background(), "SELECT 1").Scan(&result)
}

This is useful for testing the library itself, but production applications should use migrations.

Supported Databases

PostgreSQL
import "github.com/bashhack/testdb/postgres"

pool := postgres.Setup(t,
    testdb.WithMigrations("./migrations"),
    testdb.WithMigrationTool(testdb.MigrationToolTern),
)

Currently, only PostgreSQL is supported. Additional database support can be added by implementing the testdb.Provider interface.

Migration Tools

testdb supports three migration tools. Both WithMigrations() and WithMigrationTool() must be specified together.

Tern (PostgreSQL-only)

Install: go install github.com/jackc/tern/v2@latest

postgres.Setup(t,
    testdb.WithMigrations("./migrations"),
    testdb.WithMigrationTool(testdb.MigrationToolTern),
)
Goose (Multi-database)

Install: go install github.com/pressly/goose/v3/cmd/goose@latest

postgres.Setup(t,
    testdb.WithMigrations("./migrations"),
    testdb.WithMigrationTool(testdb.MigrationToolGoose),
)
golang-migrate (Multi-database)

Install: go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest

postgres.Setup(t,
    testdb.WithMigrations("./migrations"),
    testdb.WithMigrationTool(testdb.MigrationToolMigrate),
)

How It Works

testdb leverages PostgreSQL's CREATE DATABASE command for true isolation:

  1. Generates unique name - Combines prefix, nanosecond timestamp, and random suffix: test_1699564231_a1b2c3d4
  2. Creates database - Executes CREATE DATABASE via admin connection to postgres database
  3. Runs migrations - If configured, executes the specified migration tool against the new database
  4. Returns connection - Initializes a database connection using the specified initializer (e.g., *pgxpool.Pool, *sql.DB, or custom type)
  5. Registers cleanup - Uses t.Cleanup() to ensure cleanup even if test panics
  6. Terminates connections - On cleanup, forcefully closes all connections via pg_terminate_backend
  7. Drops database - Executes DROP DATABASE to remove the test database

Examples

See postgres/example_test.go for runnable examples demonstrating:

  • Basic database creation and isolation
  • Using Tern, Goose, and golang-migrate
  • Running tests concurrently with t.Parallel()
  • Custom prefixes and configuration options
  • Using GORM and other ORMs
  • Working with database/sql

All examples are also visible in the package documentation.

Requirements

  • PostgreSQL server running locally or accessible
  • Migration tool (tern, goose, or migrate) in PATH if using migrations
  • Go 1.24+

License

MIT License - see LICENSE

Documentation

Overview

Package testdb provides isolated test database instances for Go tests. It enables true parallel testing by creating a unique database for each test, with migration support and optional automatic cleanup.

Supported databases:

  • PostgreSQL (github.com/bashhack/testdb/postgres)

Basic usage with PostgreSQL (automatic cleanup via postgres.Setup):

import (
    "github.com/bashhack/testdb"
    "github.com/bashhack/testdb/postgres"
)

func TestUsers(t *testing.T) {
    pool := postgres.Setup(t,
        testdb.WithMigrations("./migrations"),
        testdb.WithMigrationTool(testdb.MigrationToolTern))
    // Use pool for testing...
    // Cleanup is automatic via t.Cleanup()
}

API Levels:

The library provides three levels of API with different use cases and cleanup behavior:

Level 1 - postgres.Setup() [Recommended for most users]:

  • Use when: Working with pgx/pgxpool directly
  • Returns: *pgxpool.Pool (ready to use)
  • Cleanup: Automatic via t.Cleanup() - DO NOT call Close() manually
  • Best for: Standard PostgreSQL testing with pgx

Level 2 - postgres.New() [For custom database wrappers]:

  • Use when: Using GORM, sqlx, ent, or custom initialization
  • Returns: *testdb.TestDatabase with custom entity
  • Cleanup: Automatic via t.Cleanup() - DO NOT call db.Close() manually
  • Best for: Custom ORMs, connection wrappers, or when you need the TestDatabase

Level 3 - testdb.New() [Low-level API]:

  • Use when: Need manual cleanup control or implementing custom providers
  • Returns: *testdb.TestDatabase
  • Cleanup: Manual - YOU MUST call defer db.Close()
  • Best for: Advanced use cases requiring cleanup timing control

See the postgres.Setup(), postgres.New(), and Close() documentation for details.

Example (CustomInitializer)

Example_customInitializer shows implementing a custom initializer for integration with ORMs or custom database types.

package main

import (
	"context"
	"fmt"
	"testing"

	"github.com/bashhack/testdb"
	"github.com/bashhack/testdb/postgres"
)

func main() {
	t := &testing.T{}

	provider := &postgres.PostgresProvider{}
	initializer := &exampleInitializer{}

	db, err := testdb.New(t, provider, initializer,
		testdb.WithMigrations("./testdata/postgres/migrations_migrate"),
		testdb.WithMigrationTool(testdb.MigrationToolMigrate),
	)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}
	defer func() {
		if err := db.Close(); err != nil {
			fmt.Printf("Failed to close database: %v\n", err)
		}
	}()

	// Type assert to your custom type
	// myDB := db.Entity().(*myapp.DB)
	fmt.Println("Custom initializer setup complete")

}

// exampleInitializer is a custom initializer for demonstration.
type exampleInitializer struct{}

func (e *exampleInitializer) InitializeTestDatabase(ctx context.Context, dsn string) (any, error) {

	return nil, nil
}
Output:

Custom initializer setup complete
Example (DsnOnly)

Example_dsnOnly demonstrates using testdb to get just a DSN without automatic connection initialization.

package main

import (
	"fmt"
	"testing"

	"github.com/bashhack/testdb"
	"github.com/bashhack/testdb/postgres"
)

func main() {
	t := &testing.T{}

	provider := &postgres.PostgresProvider{}

	// Pass nil initializer to skip connection initialization
	db, err := testdb.New(t, provider, nil,
		testdb.WithMigrations("./testdata/postgres/migrations_migrate"),
		testdb.WithMigrationTool(testdb.MigrationToolMigrate),
	)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}
	defer func() {
		if err := db.Close(); err != nil {
			fmt.Printf("Failed to close database: %v\n", err)
		}
	}()

	// Use the DSN with your own connection logic
	dsn := db.DSN
	_ = dsn // Connect with your preferred client
	fmt.Println("DSN created successfully")

}
Output:

DSN created successfully
Example (New)

Example_new demonstrates using testdb.New with a custom initializer for full control over database initialization.

package main

import (
	"fmt"
	"testing"

	"github.com/bashhack/testdb"
	"github.com/bashhack/testdb/postgres"
)

func main() {
	t := &testing.T{}

	provider := &postgres.PostgresProvider{}
	initializer := &postgres.PoolInitializer{}

	db, err := testdb.New(t, provider, initializer,
		testdb.WithMigrations("./testdata/postgres/migrations_migrate"),
		testdb.WithMigrationTool(testdb.MigrationToolMigrate),
		testdb.WithDBPrefix("myapp_test"),
	)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}
	defer func() {
		if err := db.Close(); err != nil {
			fmt.Printf("Failed to close database: %v\n", err)
		}
	}()

	// DSN is always available
	fmt.Println("Database created successfully")

	if err := db.RunMigrations(); err != nil {
		fmt.Printf("Migration error: %v\n", err)
		return
	}
	fmt.Println("Migrations completed")

}
Output:

Database created successfully
Migrations completed

Index

Examples

Constants

View Source
const (
	// MaxDBPrefixLength is the maximum recommended length for database name prefixes.
	//
	// This limit is intentionally based on the most restrictive database to ensure
	// consistent behavior across all supported databases:
	//   - PostgreSQL: 63 bytes (most restrictive)
	//   - MySQL: 64 characters
	//   - SQLite: effectively unlimited
	//
	// Database name format: prefix_timestamp_random (prefix + 29 chars)
	// To avoid truncation: prefix + 29 <= 63, therefore prefix <= 34
	//
	// Design decision: I'm using the most restrictive limit (PostgreSQL's 63-byte limit)
	// for all databases rather than implementing database-specific validation. This
	// provides a consistent, safe experience and simplifies the API. A 34-character
	// prefix is sufficient for all practical use cases.
	MaxDBPrefixLength = 34
)

Variables

View Source
var (
	// ErrNilProvider is returned when a nil provider is passed to New().
	ErrNilProvider = errors.New("provider cannot be nil")

	// ErrNoMigrationDir is returned when RunMigrations is called without a migration directory.
	ErrNoMigrationDir = errors.New("migration directory not set")

	// ErrUnknownMigrationTool is returned when an unknown migration tool is configured.
	ErrUnknownMigrationTool = errors.New("unknown migration tool")

	// ErrMigrationToolWithoutDir is returned when a migration tool is specified without a directory.
	ErrMigrationToolWithoutDir = errors.New("migration tool specified but migration directory not set")

	// ErrMigrationDirWithoutTool is returned when a migration directory is specified without a tool.
	ErrMigrationDirWithoutTool = errors.New("migration directory specified but migration tool not set")

	// ErrPrefixTooLong is returned when the database prefix would cause identifier truncation.
	ErrPrefixTooLong = errors.New("database prefix too long: would exceed database identifier limit")
)

Functions

func ResolveAdminDSN

func ResolveAdminDSN(cfg Config, defaultDSN string) string

ResolveAdminDSN resolves the admin DSN using a consistent priority order. This helper consolidates the DSN resolution logic to avoid duplication across database-specific providers (PostgreSQL, MySQL, SQLite, etc.).

The library automatically discovers the admin DSN so users don't need to provide it unless they have custom connection requirements.

Resolution order:

  1. cfg.AdminDSNOverride (explicit user override via WithAdminDSN)
  2. TEST_DATABASE_URL environment variable
  3. DATABASE_URL environment variable
  4. defaultDSN (database-specific default)

Example usage in provider initialization:

adminDSN := testdb.ResolveAdminDSN(cfg, "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable")

Types

type Config

type Config struct {
	// AdminDSNOverride is an optional connection string override for creating/dropping test databases.
	// The user specified here must have privileges to create and drop databases.
	//
	// For PostgreSQL, this is typically the 'postgres' database.
	//
	// If not specified, the library automatically discovers the admin DSN from:
	//   1. TEST_DATABASE_URL environment variable
	//   2. DATABASE_URL environment variable
	//   3. Database-specific defaults (e.g., postgres://postgres:postgres@localhost:5432/postgres)
	//
	// Most users don't need to set this - automatic discovery works in most cases.
	// Use this when you need custom admin credentials or connection settings.
	AdminDSNOverride string

	// MigrationDir is the absolute or relative path to migration files.
	// If set, MigrationTool must also be set (and vice versa).
	//
	// Example: "./migrations" or "/path/to/project/migrations"
	MigrationDir string

	// MigrationTool specifies which migration tool to use.
	// Supported: "tern", "goose", "migrate"
	// If set, MigrationDir must also be set (and vice versa).
	//
	// Example: testdb.MigrationToolTern, testdb.MigrationToolGoose, testdb.MigrationToolMigrate
	MigrationTool MigrationTool

	// MigrationToolPath is the path to the migration tool binary.
	// If empty, the tool is assumed to be in PATH.
	//
	// Example: "/usr/local/bin/tern"
	MigrationToolPath string

	// DBPrefix is prepended to test database names.
	// Useful for identifying test databases in a shared environment.
	//
	// Default: "test"
	// Example database name: "test_1699564231_a1b2c3d4"
	DBPrefix string

	// Verbose enables logging of database operations.
	// When false (default), testdb operates silently.
	// When true, logs database creation, cleanup, and migration completion.
	//
	// Default: false
	Verbose bool
}

Config holds the configuration for test database creation and management.

func DefaultConfig

func DefaultConfig() Config

DefaultConfig returns a Config with reasonable defaults.

type DBInitializer

type DBInitializer interface {
	// InitializeTestDatabase creates and initializes a database connection/pool.
	// It receives a DSN (Data Source Name) for connecting to the test database
	// and returns an any that should be type-asserted by the caller to
	// their specific database entity type.
	//
	// Parameters:
	//   - ctx: Context for cancellation and timeouts
	//   - dsn: Connection string for the test database
	//
	// Returns:
	//   - any: Database entity (type assert to your specific type)
	//   - error: Any initialization errors
	InitializeTestDatabase(ctx context.Context, dsn string) (any, error)
}

DBInitializer defines the interface for custom database initialization in tests.

When You Need a Custom Initializer

Most users should use database-specific convenience functions (e.g., postgres.Setup) and don't need to implement this interface. Implement DBInitializer when:

1. Using an ORM (GORM, ent, SQLBoiler):

type GormInitializer struct{}
func (g *GormInitializer) InitializeTestDatabase(ctx context.Context, dsn string) (any, error) {
    return gorm.Open(postgres.Open(dsn), &gorm.Config{})
}

2. Using sqlx for struct scanning:

type SqlxInitializer struct{}
func (s *SqlxInitializer) InitializeTestDatabase(ctx context.Context, dsn string) (any, error) {
    return sqlx.Connect("postgres", dsn)
}

3. Wrapping connections in your application's custom type:

type AppDB struct {
    Pool    *pgxpool.Pool
    Timeout time.Duration
}
type AppDBInitializer struct{}
func (a *AppDBInitializer) InitializeTestDatabase(ctx context.Context, dsn string) (any, error) {
    pool, err := pgxpool.New(ctx, dsn)
    if err != nil {
        return nil, err
    }
    return &AppDB{Pool: pool, Timeout: 30 * time.Second}, nil
}

4. Custom connection pooling settings:

type CustomPoolInitializer struct {
    MaxConns int32
}
func (c *CustomPoolInitializer) InitializeTestDatabase(ctx context.Context, dsn string) (any, error) {
    config, _ := pgxpool.ParseConfig(dsn)
    config.MaxConns = c.MaxConns
    config.MinConns = 2
    config.MaxConnLifetime = 1 * time.Hour
    return pgxpool.NewWithConfig(ctx, config)
}

5. Adding tracing/logging/instrumentation:

type TracedInitializer struct{}
func (t *TracedInitializer) InitializeTestDatabase(ctx context.Context, dsn string) (any, error) {
    config, _ := pgxpool.ParseConfig(dsn)
    config.ConnConfig.Tracer = &MyQueryTracer{}
    return pgxpool.NewWithConfig(ctx, config)
}

Why Use a Custom Initializer?

The key benefit is that your tests use the SAME database type as your application code. If your app functions expect *gorm.DB, your tests should use *gorm.DB too. If your app uses a custom AppDB wrapper, your tests should use that wrapper. This ensures your tests accurately reflect real-world usage.

type Error

type Error struct {
	// Op is the operation that failed (e.g., "provider.Initialize").
	Op string

	// Err is the underlying error.
	Err error
}

Error represents a testdb error with operation context.

func (*Error) Error

func (e *Error) Error() string

func (*Error) Unwrap

func (e *Error) Unwrap() error

type MigrationTool

type MigrationTool string

MigrationTool represents supported database migration tools.

const (
	// MigrationToolTern represents the 'tern' migration tool.
	// External dependency: Must be installed separately and available in PATH.
	// See: https://github.com/jackc/tern
	// PostgreSQL only.
	MigrationToolTern MigrationTool = "tern"

	// MigrationToolGoose represents the 'goose' migration tool.
	// External dependency: Must be installed separately and available in PATH.
	// See: https://github.com/pressly/goose
	// Supports PostgreSQL, MySQL, SQLite.
	MigrationToolGoose MigrationTool = "goose"

	// MigrationToolMigrate represents the 'golang-migrate/migrate' migration tool.
	// External dependency: Must be installed separately and available in PATH.
	// See: https://github.com/golang-migrate/migrate
	// Supports PostgreSQL, MySQL, SQLite, MongoDB, and many others.
	MigrationToolMigrate MigrationTool = "migrate"
)

type Option

type Option func(*Config)

Option is a functional option for configuring test databases.

func WithAdminDSN

func WithAdminDSN(dsn string) Option

WithAdminDSN overrides the admin connection string. Use this when your database is not on localhost or uses non-default credentials.

Most users don't need this - the library automatically discovers the admin DSN from environment variables (TEST_DATABASE_URL, DATABASE_URL) or uses sensible defaults.

Example:

testdb.WithAdminDSN("postgres://user:pass@db.example.com:5432/postgres")

func WithDBPrefix

func WithDBPrefix(prefix string) Option

WithDBPrefix sets the database name prefix. Useful for identifying test databases in a shared environment.

Example:

testdb.WithDBPrefix("myapp_test")
// Results in database names like: myapp_test_1699564231_a1b2c3d4

func WithMigrationTool

func WithMigrationTool(tool MigrationTool) Option

WithMigrationTool sets the migration tool to use. You must also set WithMigrations() when using this option. Valid values: testdb.MigrationToolTern, testdb.MigrationToolGoose, testdb.MigrationToolMigrate

Example:

testdb.WithMigrationTool(testdb.MigrationToolGoose)
testdb.WithMigrationTool(testdb.MigrationToolMigrate)

func WithMigrationToolPath

func WithMigrationToolPath(path string) Option

WithMigrationToolPath sets the path to the migration tool binary. Use this if the tool is not in your PATH.

Example:

testdb.WithMigrationToolPath("/usr/local/bin/goose")

func WithMigrations

func WithMigrations(dir string) Option

WithMigrations sets the migration directory. The directory should contain your migration files. You must also set WithMigrationTool() when using this option.

Example:

testdb.WithMigrations("./migrations")
testdb.WithMigrations("../../db/migrations")

func WithVerbose

func WithVerbose() Option

WithVerbose enables verbose logging of database operations. By default, testdb operates silently. Enable this for debugging.

Example:

testdb.WithVerbose()

type Provider

type Provider interface {
	// Initialize sets up the provider with admin credentials.
	// This establishes a connection to the admin/system database.
	Initialize(ctx context.Context, cfg Config) error

	// CreateDatabase creates a new database with the given name.
	CreateDatabase(ctx context.Context, name string) error

	// DropDatabase drops an existing database.
	DropDatabase(ctx context.Context, name string) error

	// TerminateConnections forcefully closes all connections to a database.
	// This is necessary before dropping a database.
	TerminateConnections(ctx context.Context, name string) error

	// BuildDSN constructs a connection string for the given database name.
	BuildDSN(dbName string) (string, error)

	// ResolvedAdminDSN returns the resolved admin DSN being used by this provider.
	// This is the actual DSN after resolving user overrides, environment variables, and defaults.
	// Useful for migrations and other operations that need admin credentials.
	ResolvedAdminDSN() string

	// Cleanup performs any necessary provider cleanup (e.g., closing admin connections).
	Cleanup(ctx context.Context) error
}

Provider defines database-specific operations that must be implemented for each supported database system (PostgreSQL, MySQL, SQLite, MongoDB).

This interface is typically implemented by database-specific packages and is not usually used directly by end users.

type TestDatabase

type TestDatabase struct {
	// contains filtered or unexported fields
}

TestDatabase represents an isolated test database instance. It manages the complete lifecycle of a test database, including:

  • Creation and initialization
  • Migration management
  • Connection management
  • Cleanup and resource disposal

func New

func New(t testing.TB, provider Provider, initializer DBInitializer, opts ...Option) (*TestDatabase, error)

New creates a test database using the provided provider and optional initializer.

This is the low-level API for creating test databases. Most users should use the database-specific convenience functions instead (e.g., postgres.Setup()).

If initializer is nil, no automatic connection initialization is performed. The DSN field will still be available for manual connection setup.

Parameters:

  • t: Testing context for logging and cleanup
  • provider: Database-specific provider implementation
  • initializer: Optional custom initializer (can be nil)
  • opts: Configuration options

Returns:

  • *TestDatabase: Handle to manage the test database
  • error: Any errors during creation

Example:

provider := &postgres.PostgresProvider{}
initializer := &postgres.PoolInitializer{}
db, err := testdb.New(t, provider, initializer,
    testdb.WithMigrations("./migrations"),
    testdb.WithMigrationTool(testdb.MigrationToolTern),
)
if err != nil {
    t.Fatal(err)
}
defer db.Close()

pool := db.Entity().(*pgxpool.Pool)

func (*TestDatabase) Close

func (td *TestDatabase) Close() error

Close cleans up the test database and associated resources.

This method:

  1. Terminates all active connections to the database
  2. Drops the database
  3. Cleans up provider resources

When to call Close():

  • Manual cleanup is required when using the low-level testdb.New() API directly
  • Cleanup is AUTOMATIC when using database-specific Setup/New functions (e.g., postgres.Setup(), postgres.New()) - they register cleanup via t.Cleanup()

Example with low-level API (manual cleanup required):

db, err := testdb.New(t, provider, initializer)
if err != nil {
    t.Fatal(err)
}
defer db.Close()

Or with t.Cleanup:

db, err := testdb.New(t, provider, initializer)
if err != nil {
    t.Fatal(err)
}
t.Cleanup(func() { db.Close() })

Example with high-level API (automatic cleanup):

pool := postgres.Setup(t)  // Cleanup registered automatically
// No need to call Close() - handled by t.Cleanup()

func (*TestDatabase) Config

func (td *TestDatabase) Config() Config

Config returns the configuration used to create this database.

func (*TestDatabase) DSN

func (td *TestDatabase) DSN() string

DSN returns the connection string for this test database.

func (*TestDatabase) Entity

func (td *TestDatabase) Entity() any

Entity returns the initialized database entity. This is only available if a DBInitializer was provided to New().

Type assertions are required since the entity type depends on your DBInitializer.

Common usage (direct assertion - panics on type mismatch):

pool := db.Entity().(*pgxpool.Pool)
gormDB := db.Entity().(*gorm.DB)
sqlxDB := db.Entity().(*sqlx.DB)

Safe assertion (checks type before asserting):

entity := db.Entity()
pool, ok := entity.(*pgxpool.Pool)
if !ok {
    t.Fatalf("expected *pgxpool.Pool, got %T", entity)
}

Note: Since you control the DBInitializer, direct assertions are usually safe. Panics during test setup help catch initialization bugs early.

func (*TestDatabase) Name

func (td *TestDatabase) Name() string

Name returns the unique database name for this test database.

func (*TestDatabase) RunMigrations

func (td *TestDatabase) RunMigrations() error

RunMigrations executes database migrations using the configured migration tool. The migration directory and tool must both be set via WithMigrations() and WithMigrationTool() options.

Supported migration tools:

  • Tern (github.com/jackc/tern) - PostgreSQL only
  • Goose (github.com/pressly/goose) - PostgreSQL, MySQL, SQLite
  • golang-migrate (github.com/golang-migrate/migrate) - PostgreSQL, MySQL, SQLite, MongoDB, and more

The migration tool binary must be available in PATH or specified via WithMigrationToolPath() option.

Returns an error if migrations fail. The database is NOT automatically cleaned up on migration failure - call Close() manually if needed.

Example:

db, err := testdb.New(t, provider, initializer,
    testdb.WithMigrations("./migrations"),
    testdb.WithMigrationTool(testdb.MigrationToolTern),
)
if err != nil {
    t.Fatal(err)
}
defer db.Close()

if err := db.RunMigrations(); err != nil {
    t.Fatalf("migrations failed: %v", err)
}

Directories

Path Synopsis
Package postgres provides PostgreSQL test database support for testdb.
Package postgres provides PostgreSQL test database support for testdb.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL