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/migrate/goose"
"github.com/bashhack/testdb/postgres"
)
func TestUsers(t *testing.T) {
pool := postgres.Setup(t,
testdb.WithMigrator(goose.Dir("./migrations")))
// 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.
t := newExampleT()
defer t.done()
provider := &postgres.PostgresProvider{}
initializer := &exampleInitializer{}
db, err := testdb.New(t, provider, initializer,
testdb.WithMigrator(postgres.SQLFiles("./testdata/postgres/schema")),
)
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")
Output: Custom initializer setup complete
Example (DsnOnly) ¶
Example_dsnOnly demonstrates using testdb to get just a DSN without automatic connection initialization.
t := newExampleT()
defer t.done()
provider := &postgres.PostgresProvider{}
// Pass nil initializer to skip connection initialization
db, err := testdb.New(t, provider, nil,
testdb.WithMigrator(postgres.SQLFiles("./testdata/postgres/schema")),
)
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.
t := newExampleT()
defer t.done()
provider := &postgres.PostgresProvider{}
initializer := &postgres.PoolInitializer{}
db, err := testdb.New(t, provider, initializer,
testdb.WithMigrator(postgres.SQLFiles("./testdata/postgres/schema")),
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(context.Background()); err != nil {
fmt.Printf("Migration error: %v\n", err)
return
}
fmt.Println("Migrations completed")
Output: Database created successfully Migrations completed
Index ¶
- Constants
- Variables
- func ContextOf(t testing.TB) context.Context
- func NewDatabaseName(prefix string) (string, error)
- func RegisterCleanup(t testing.TB, db *TestDatabase)
- func ResolveAdminDSN(cfg Config, defaultDSN string, providerEnv ...string) string
- func RunConfiguredMigrations(t testing.TB, db *TestDatabase, callerName string)
- type Config
- type DBInitializer
- type Error
- type InspectCommander
- type Migrator
- type MigratorFunc
- type Option
- type Provider
- type TemplateProvider
- type TestDatabase
Examples ¶
Constants ¶
const KeepOnFailureEnv = "TESTDB_KEEP_ON_FAILURE"
KeepOnFailureEnv is the environment variable that turns on KeepOnFailure for a run.
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 ¶
var ( // ErrNilProvider is returned when a nil provider is passed to New(). ErrNilProvider = errors.New("provider cannot be nil") // ErrNoMigrator is returned when RunMigrations is called without a Migrator configured. ErrNoMigrator = errors.New("no migrator configured") // ErrTemplatesNotSupported is returned by New when WithTemplate is used // with a provider that does not implement TemplateProvider. ErrTemplatesNotSupported = errors.New("templates are not supported") // 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 ContextOf ¶
ContextOf returns t.Context(), which is cancelled when the test ends, so database setup and migrations stop when a test times out. A zero testing.T, as runnable examples use, has no context; then it returns context.Background(). Database-specific packages use it when calling RunMigrations on a test's behalf.
func NewDatabaseName ¶
NewDatabaseName returns a unique database name with the given prefix, in the form every test database gets: {prefix}_{timestamp}_{random}, for example test_1699564231_a1b2c3d4. Providers that prepare databases ahead of a test use it so those look like any other test database.
func RegisterCleanup ¶
func RegisterCleanup(t testing.TB, db *TestDatabase)
RegisterCleanup arranges for the database to go away when the test ends: the entity is closed first if it implements io.Closer, then Close drops the database. With KeepOnFailure, a failed test's database is kept instead (see Keep).
func ResolveAdminDSN ¶
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:
- cfg.AdminDSNOverride (explicit user override via WithAdminDSN)
- The provider's own environment variables, in the order given (e.g. TEST_POSTGRES_URL), so a project that tests against two databases can point each provider at its own server
- TEST_DATABASE_URL environment variable
- DATABASE_URL environment variable
- defaultDSN (database-specific default)
Example usage in provider initialization:
adminDSN := testdb.ResolveAdminDSN(cfg,
"postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable",
"TEST_POSTGRES_URL")
func RunConfiguredMigrations ¶
func RunConfiguredMigrations(t testing.TB, db *TestDatabase, callerName string)
RunConfiguredMigrations runs the Migrator set with WithMigrator, if any. On failure it drops the database (or keeps it, under KeepOnFailure, since a half-applied migration is exactly what one wants to look at) and calls t.Fatalf, so it does not return. callerName is the function the test called, for the message.
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. A database-specific environment variable (e.g., TEST_POSTGRES_URL)
// 2. TEST_DATABASE_URL environment variable
// 3. DATABASE_URL environment variable
// 4. 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
// Migrator prepares each new test database, typically by running the
// project's migrations. nil (the default) leaves the database empty.
// See WithMigrator.
Migrator Migrator
// 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
// TemplateName specifies a template database to clone instead of creating a fresh database.
// Templates are significantly faster than fresh database creation, especially for large schemas
// or databases with pre-seeded data.
//
// If empty (default), a fresh database is created.
// If specified, the template database must already exist (see postgres.EnsureTemplate).
//
// Example: "us_region" or "seeded_schema"
//
// Performance impact:
// - Fresh database: ~12-15ms (PostgreSQL CREATE DATABASE)
// - Template clone: ~3-5ms (PostgreSQL CREATE DATABASE WITH TEMPLATE)
TemplateName 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
// KeepOnFailure leaves a test's database in place when the test fails,
// and logs a connection string for it, so the data can be inspected.
// See WithKeepOnFailure.
//
// Default: false, or true when the TESTDB_KEEP_ON_FAILURE environment
// variable is set to 1, true, yes or on.
KeepOnFailure 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.
type InspectCommander ¶
InspectCommander is implemented by providers that can say how to open a database in that database's own command-line client (psql, mysql, ...). The kept-database log line (see Keep and WithKeepOnFailure) shows that command; without it, it shows the DSN.
type Migrator ¶
Migrator prepares a freshly created, empty database: it runs migrations, loads fixtures, or does whatever the tests need to find in place. It receives the DSN of the database to prepare.
Adapters for the common migration tools live in their own modules, so a project only depends on the tool it uses:
- github.com/bashhack/testdb/migrate/goose
- github.com/bashhack/testdb/migrate/tern
- github.com/bashhack/testdb/migrate/golangmigrate
For plain SQL without a tool, see postgres.SQLFiles and postgres.LoadSQLFile. Template setup functions have the same shape (postgres.SetupFunc is a MigratorFunc), so one Migrator serves both per-test databases and templates.
type MigratorFunc ¶
MigratorFunc adapts a function to the Migrator interface.
type Option ¶
type Option func(*Config)
Option is a functional option for configuring test databases.
func WithAdminDSN ¶
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 ¶
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 WithKeepOnFailure ¶
func WithKeepOnFailure() Option
WithKeepOnFailure keeps a test's database when the test fails, instead of dropping it, and logs a psql command for it. Passing tests still have their databases dropped. Use it to look at the data a failing test left behind.
The TESTDB_KEEP_ON_FAILURE environment variable (1, true, yes or on) turns this on for a whole run without changing code:
TESTDB_KEEP_ON_FAILURE=1 go test ./... -run TestCheckout
Kept databases are yours to drop afterwards; WithDBPrefix makes them easy to spot. Leave this off in CI, where nobody is going to look and the databases would only pile up.
func WithMigrator ¶
WithMigrator sets how each new test database is prepared before the test gets it. Setup and New run the migrator right after creating the database.
Example:
pool := postgres.Setup(t, testdb.WithMigrator(goose.Dir("./migrations")))
pool := postgres.Setup(t, testdb.WithMigrator(postgres.SQLFiles("./schema")))
func WithTemplate ¶
WithTemplate configures the test database to be created from a template database. This is significantly faster than creating a fresh database, especially for large schemas or databases with pre-seeded data.
The template database must already exist (see postgres.EnsureTemplate). If the template doesn't exist, database creation will fail.
Performance impact:
- Fresh database: ~12-15ms
- Template clone: ~3-5ms (3-5x faster)
Common use cases:
- Large SQL dumps (fixtures) loaded once, cloned per test
- Different fixture sets for different test suites (us_region, eu_region)
- Testing migrations from specific schema versions
Example:
db := postgres.New(t, &postgres.PoolInitializer{},
testdb.WithTemplate("us_region"))
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 TemplateProvider ¶
type TemplateProvider interface {
Provider
// CreateDatabaseFromTemplate makes a database cloned from the template
// and returns its name: requestedName, or a different one when the
// provider had a clone ready in advance and cannot rename it. The
// template database must already exist.
CreateDatabaseFromTemplate(ctx context.Context, requestedName, templateName string) (actualName string, err error)
// TemplateExists checks if a template database exists.
// This is useful for debugging and conditional template creation.
TemplateExists(ctx context.Context, name string) (bool, error)
}
TemplateProvider is implemented by providers whose database can clone one database from another, which is what templates (see WithTemplate) rely on. PostgreSQL has CREATE DATABASE ... TEMPLATE and implements it. A provider for a database without an equivalent implements only Provider, and New returns ErrTemplatesNotSupported if a template is requested with it.
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.WithMigrator(goose.Dir("./migrations")),
)
if err != nil {
t.Fatal(err)
}
defer db.Close()
pool := db.Entity().(*pgxpool.Pool)
Creation uses t.Context(), so it is cancelled when the test times out.
func (*TestDatabase) Close ¶
func (td *TestDatabase) Close() error
Close cleans up the test database and associated resources.
This method:
- Terminates all active connections to the database
- Drops the database
- 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) Keep ¶
func (td *TestDatabase) Keep()
Keep makes Close leave the database in place instead of dropping it, and log a connection string for it. The provider's own resources are still released. postgres.Setup and postgres.New call this for a failed test when KeepOnFailure is set; with the low-level API you decide:
db, err := testdb.New(t, provider, nil)
...
if t.Failed() {
db.Keep()
}
db.Close()
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(ctx context.Context) error
RunMigrations runs the configured Migrator (see WithMigrator) against this database. postgres.Setup and postgres.New call it for you right after creating the database; it is exposed for the low-level testdb.New path, where you choose when it runs.
It returns ErrNoMigrator (wrapped) if no Migrator was configured. The database is not dropped on failure; call Close if you want that.
Example:
db, err := testdb.New(t, provider, initializer,
testdb.WithMigrator(goose.Dir("./migrations")))
if err != nil {
t.Fatal(err)
}
defer db.Close()
if err := db.RunMigrations(t.Context()); err != nil {
t.Fatalf("migrations failed: %v", err)
}
Directories
¶
| Path | Synopsis |
|---|---|
|
migrate
|
|
|
tern
module
|
|
|
Package mysql provides isolated MySQL databases for tests: each test gets its own database, created with CREATE DATABASE and dropped when the test ends, so tests can run in parallel against one server.
|
Package mysql provides isolated MySQL databases for tests: each test gets its own database, created with CREATE DATABASE and dropped when the test ends, so tests can run in parallel against one server. |
|
internal/concurrencytest
Package concurrencytest is a test package that mysql's multi-process template tests compile and run as separate processes.
|
Package concurrencytest is a test package that mysql's multi-process template tests compile and run as separate processes. |
|
Package postgres provides PostgreSQL test database support for testdb.
|
Package postgres provides PostgreSQL test database support for testdb. |
|
internal/concurrencytest
Package concurrencytest is a test package that postgres's multi-process template tests compile and run as separate processes.
|
Package concurrencytest is a test package that postgres's multi-process template tests compile and run as separate processes. |