testenv

A comprehensive Go framework for managing PostgreSQL test environments with container support, connection pooling, and automatic migrations.
Overview
testenv provides a clean, composable system for setting up and managing database environments in tests and production code. It offers flexible connection management with support for Docker containers, connection pooling, multiple reuse strategies, and automatic migration application.
Features
- Environment Management: High-level abstractions for managing database connections
- Container Support: Seamless integration with testcontainers for isolated test environments
- Connection Pooling: Efficient connection management via pgx
- Multiple Reuse Strategies:
- Create new database per connection (full isolation)
- Create new schema per connection (good isolation, better performance)
- Raw connection (no additional isolation)
- Automatic Migrations: Apply migrations automatically when connecting
- Standard Library Compatibility: Works with both pgx and standard
*sql.DB
- Thread Safe: All components are safe for concurrent use
- Test Helpers: Built-in utilities for test cleanup and lifecycle management
Installation
go get go.amidman.dev/testenv
Quick Start
Basic Usage
package main
import (
"context"
"fmt"
"testing"
"go.amidman.dev/testenv/dbenv"
"go.amidman.dev/testenv/postgresenv"
)
func TestWithDatabase(t *testing.T) {
t.Parallel()
// Create a connector
connector := postgresenv.NewConnector(
postgresenv.WithHost("localhost"),
postgresenv.WithPort(5432),
postgresenv.WithDatabase("testdb"),
postgresenv.WithUser("testuser"),
postgresenv.WithPassword("testpass"),
)
// Create environment with reuse strategy
env := postgresenv.New(
postgresenv.WithConnector(connector),
postgresenv.WithReuseStrategy(&postgresenv.ReuseStrategyCreateNewDB{}),
)
// Connect - automatically creates a new isolated database
pool, err := env.Connect(t.Context())
if err != nil {
t.Fatal(err)
}
defer pool.Close()
// Use the pool for database operations
var count int
err = pool.Pool.QueryRow(t.Context(), "SELECT COUNT(*) FROM users").Scan(&count)
if err != nil {
t.Fatal(err)
}
fmt.Printf("User count: %d\n", count)
}
Using with dbenv.UseForTesting
For simpler test setup with automatic cleanup:
import (
"testing"
"go.amidman.dev/testenv/dbenv"
)
func TestWithDbenvHelper(t *testing.T) {
t.Parallel()
// Create environment
env := createTestEnvironment()
// Get database connection - automatically closed after test
db := dbenv.UseForTesting(t, env)
// Use standard *sql.DB
var count int
err := db.QueryRow("SELECT COUNT(*) FROM users").Scan(&count)
require.NoError(t, err)
assert.Equal(t, 0, count)
}
With Migrations
import (
"testing"
"go.amidman.dev/testenv/dbenv"
"go.amidman.dev/testenv/dbenv/migrations"
)
func TestWithMigrations(t *testing.T) {
t.Parallel()
// Create environment
env := createBaseEnvironment()
// Define migrations as an option
migration := migrations.Queries(
"CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)",
"CREATE INDEX idx_users_name ON users(name)",
)
// Connect with migrations applied via option
db := dbenv.UseForTesting(t, env, migration)
// Database now has users table
_, err := db.Exec("INSERT INTO users (name) VALUES ($1)", "Alice")
require.NoError(t, err)
}
Standard Library Compatibility
// Create stdlib-compatible environment
stdlibEnv := postgresenv.NewStdlib(env)
// Connect using standard library
db, err := stdlibEnv.Connect(ctx)
if err != nil {
return err
}
defer db.Close()
// Use standard *sql.DB
rows, err := db.QueryContext(ctx, "SELECT * FROM users")
Package Structure
dbenv: Core utilities for using database environments
dbenv/migrations: Migration management utilities
postgresenv: PostgreSQL-specific environment management
minioenv: MinIO environment management (object storage)
minioenv/containers: Docker container management for MinIO
reuse: Generic resource reuse mechanism
Reuse Strategies
ReuseStrategyCreateNewDB
Creates a new database for each connection. Provides complete isolation but may be slower due to database creation overhead.
env := postgresenv.New(
postgresenv.WithReuseStrategy(&postgresenv.ReuseStrategyCreateNewDB{
NewDBName: func() string {
return "testdb_" + uuid.New().String()
},
}),
)
ReuseStrategyCreateNewSchema
Creates a new schema in an existing database. Good isolation with better performance.
env := postgresenv.New(
postgresenv.WithReuseStrategy(&postgresenv.ReuseStrategyCreateNewSchema{
DB: "mydb",
NewSchemaName: func() string {
return "testschema_" + uuid.New().String()
},
}),
)
minioenv
The minioenv package provides MinIO (S3-compatible object storage)
environment management. It mirrors the postgresenv design:
Connector owns a reuse instance, Environment applies a strategy,
and containers.Container provides a testcontainers-backed lifecycle.
The *minio.Client from github.com/minio/minio-go/v7 is concurrency-safe
and is sufficient for both admin (MakeBucket / RemoveBucket) and
app operations — no service-pool cache is needed.
Quick Start
Raw client (default)
With no reuse strategy, the environment returns a raw *minio.Client
and the user manages buckets themselves.
package main
import (
"context"
"fmt"
"testing"
"go.amidman.dev/testenv/minioenv"
"go.amidman.dev/testenv/minioenv/containers"
)
func TestWithMinio(t *testing.T) {
t.Parallel()
// Create a MinIO container and connector
cnt := containers.NewContainer()
connector := minioenv.NewConnector(cnt.ConnectorOptions()...)
// Create environment with default (raw) mode
env := minioenv.New(minioenv.WithConnector(connector))
// Connect - returns a raw *minio.Client via Client
client, err := env.Connect(t.Context())
if err != nil {
t.Fatal(err)
}
defer client.Close()
// Use the underlying *minio.Client for object operations
exists, err := client.BucketExists(t.Context(), "my-bucket")
if err != nil {
t.Fatal(err)
}
fmt.Printf("Bucket exists: %v\n", exists)
}
WithBuckets — shared, well-known buckets
WithBuckets is an EnvironmentOption that ensures the listed
buckets exist before Connect returns. Buckets are not removed
on Client.Close — they live for the entire env lifecycle and
persist across tests/runs. Use this for shared, well-known buckets.
env := minioenv.New(
minioenv.WithConnector(connector),
minioenv.WithBuckets("config", "uploads"),
)
client, _ := env.Connect(ctx)
defer client.Close()
// "config" and "uploads" are guaranteed to exist
_ = client.PutObject(ctx, "config", "app.yaml", reader)
ReuseStrategyCreateNewBucket — per-Connect isolated bucket
For per-Connect isolated buckets with automatic cleanup, use
ReuseStrategyCreateNewBucket. The actual bucket name is exposed
via Client.Bucket() because MinIO has no equivalent of SQL
search_path — the user must name the bucket explicitly when
calling PutObject / GetObject / etc.
env := minioenv.New(
minioenv.WithConnector(connector),
minioenv.WithReuseStrategy(&minioenv.ReuseStrategyCreateNewBucket{}),
)
client, _ := env.Connect(ctx)
defer client.Close() // bucket is removed on Close
// Use the actual bucket name via Client.Bucket()
err := client.PutObject(ctx, client.Bucket(), "alice.json", reader)
UseForTesting
For tests, UseForTesting mirrors dbenv.UseForTesting: it
calls t.Helper(), calls t.Fatal on connect error, and registers
a t.Cleanup to call client.Close().
func TestWithMinioForTesting(t *testing.T) {
t.Parallel()
cnt := containers.NewContainer()
connector := minioenv.NewConnector(cnt.ConnectorOptions()...)
env := minioenv.New(
minioenv.WithConnector(connector),
minioenv.WithReuseStrategy(&minioenv.ReuseStrategyCreateNewBucket{}),
)
// Get client — automatically closed after test
client := minioenv.UseForTesting(t, env)
// Use client in test
err := client.PutObject(ctx, client.Bucket(), "key", reader)
require.NoError(t, err)
}
Container
The minioenv/containers subpackage provides a testcontainers-backed
Container that manages a real MinIO Docker instance. The
container is started lazily on the first Client call (via
BeforeConnectHook) and stopped on connector.Close() (via
CloseHook).
cnt := containers.NewContainer()
connector := minioenv.NewConnector(cnt.ConnectorOptions()...)
defer connector.Close()
// Container starts automatically on first Client call
client, err := connector.Client(ctx)
// Container stops on Close (via closeHook)
connector.Close()
The default image is minio/minio:RELEASE.2024-01-16T16-07-38Z,
matching the tag pinned in testcontainers-go/modules/minio@v0.40.0.
Override with containers.WithImage("minio/minio:latest") if needed.
Reuse Strategy
ReuseStrategyCreateNewBucket
Creates a new bucket per connection, removes it on Client.Close().
Provides complete isolation between tests. The actual bucket name
is exposed via Client.Bucket().
env := minioenv.New(
minioenv.WithConnector(connector),
minioenv.WithReuseStrategy(&minioenv.ReuseStrategyCreateNewBucket{
NewBucketName: func() string {
return "my-bucket-" + uuid.New().String()
},
}),
)
Documentation
For detailed documentation, see the package docs:
License
MIT License. See LICENSE for details.