dbtestkit

package
v0.3.6 Latest Latest
Warning

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

Go to latest
Published: Jun 22, 2026 License: MIT Imports: 8 Imported by: 0

README

dbtestkit

A generic, ORM-agnostic test container toolkit for Go integration tests that need a real database.

dbtestkit boots a database (in Docker for MySQL/PostgreSQL, or on local disk for SQLite), restores a pre-baked pristine SQL dump for millisecond resets between tests, and exposes a single Reset entry-point that returns the database to a known-good state before every test.

Configuration is entirely functional-options based — no environment variables. Every project-specific concern (schema creation, data seeding, engine construction, cache invalidation) is supplied by the caller via callbacks, so the library never imports your ORM or your domain objects.


Features

  • Three backends out of the box — MySQL 8.x, PostgreSQL 16, and in-process SQLite.
  • Fast path / slow path — the first test run generates a pristine SQL dump (~8 s); subsequent runs restore it in milliseconds. Performance is preserved from the original casdoor test utils.
  • Functional options — no env vars, no globals, no init() magic. Discoverable via code completion.
  • ORM-agnostic — bring your own Engine (xorm, gorm, sqlx, plain *sql.DB). An xorm adapter ships in the xormadapter subpackage.
  • Test isolationReset clears ORM caches, restores the pristine dump, and re-runs your custom seeders before every test.
  • Race-safe — resets are serialized via a mutex so parallel tests in the same package share one container safely.
  • KISS / DRY / SOLID — small interfaces, single-responsibility files, open-closed extension via the DatabaseDriver interface.

Installation

go get github.com/seyallius/gopherbox/dbtestkit

Quick start

This example uses xorm + MySQL. The pattern is identical for any other combination — swap the driver import and the engine initializer.

package mypackage_test

import (
	"testing"

	"github.com/seyallius/gopherbox/dbtestkit"
	"github.com/seyallius/gopherbox/dbtestkit/drivers/mysql"
	"github.com/seyallius/gopherbox/dbtestkit/xormadapter"
	"github.com/xorm-io/xorm"
)

// TestMain wires up the test container once for the whole package.
func TestMain(m *testing.M) {
	dbtestkit.Run(m,
		// 1. Pick a backend.
		dbtestkit.WithDriver(mysql.New()),

		// 2. (Optional) Override credentials / image.
		dbtestkit.WithDatabase(dbtestkit.DatabaseConfig{
			Database: "myapp",
			Username: "root",
			Password: "testpass",
			Image:    "mysql:lts",
		}),

		// 3. Tell dbtestkit how to create tables (slow path only).
		dbtestkit.WithSchemaInitializer(func(env *dbtestkit.Environment) error {
			eng, _ := xorm.NewEngine("mysql", env.DSN())
			return eng.Sync2(&User{}, &Org{}, &Token{})
		}),

		// 4. Tell dbtestkit how to seed base data (slow path only).
		dbtestkit.WithDataInitializer(func(env *dbtestkit.Environment) error {
			// Insert your default org, app, admin user, etc.
			return seedDefaults(env)
		}),

		// 5. Tell dbtestkit how to build your engine from the DSN.
		dbtestkit.WithEngineInitializer(func(env *dbtestkit.Environment) (dbtestkit.Engine, error) {
			eng, err := xorm.NewEngine("mysql", env.DSN())
			if err != nil {
				return nil, err
			}
			return xormadapter.New(eng), nil
		}),

		// 6. (Optional) Flush ORM caches before every reset.
		dbtestkit.WithCacheInvalidator(func(env *dbtestkit.Environment) error {
			// e.g. clear your Ristretto store, in-memory lookup tables, etc.
			return nil
		}),

		// 7. (Optional) Per-test seeders.
		dbtestkit.WithSeeders(func(env *dbtestkit.Environment) error {
			// Insert test-specific fixtures.
			return nil
		}),
	)
}

// Each test calls Reset at the top to get a clean database.
func TestUserSignup(t *testing.T) {
	if err := dbtestkit.Reset(t, "TestUserSignup"); err != nil {
		t.Fatalf("reset: %v", err)
	}

	// ... your test logic ...
}

How it works

Setup phase (once per test package)

Run performs a one-time setup:

  1. Resolves the project root by walking up until go.mod is found.
  2. Boots the backend (Docker container for MySQL/Postgres, local file for SQLite).
  3. Fast path (default, if a pristine dump exists): restore the existing dump into the backend (~1 s).
  4. Slow path (first run, or WithGeneratePristine(true)):
    • Call the user's SchemaInitializer (e.g. xorm.Sync2).
    • Call the user's DataInitializer (e.g. insert default rows).
    • Generate a fresh pristine dump (mysqldump, pg_dump, or SQLite file copy).
  5. Call the user's EngineInitializer with the live DSN.
  6. Ping the engine to verify connectivity.
  7. Hand control to m.Run().
Reset phase (per test)

Reset is called at the top of every test that needs a clean DB:

  1. Acquire a global mutex (parallel tests serialize on resets).
  2. Call the user's CacheInvalidator (if provided) to flush ORM-level caches.
  3. Call engine.ClearCache() to flush xorm's Ristretto store (or no-op for ORMs without a cache).
  4. Restore the pristine dump into the backend (MySQL/Postgres: mysql < dump.sql; SQLite: file copy).
  5. Run each registered Seeder in registration order.
Performance characteristics

Carried over from the original casdoor test utils:

Operation MySQL Postgres SQLite
First-run setup ~8.7 s ~6 s ~3 s
Subsequent setup ~1.2 s ~1.5 s ~50 ms
Per-test reset ~30 ms ~40 ms ~5 ms

The first-run cost is dominated by Sync2 + base data insertion. Subsequent runs restore the pre-baked dump. Per-test resets pipe the dump back into the container via the DB CLI (mysql, psql) or copy a snapshot file (SQLite) — both are dramatically faster than re-running ORM inserts.


Configuration reference

All configuration is supplied via WithXxx options. There are no environment variables.

Required options
Option Purpose
WithDriver(d) The database driver instance (e.g. mysql.New()).
WithSchemaInitializer(fn) Callback that creates tables (slow path only).
WithDataInitializer(fn) Callback that seeds base data (slow path only).
WithEngineInitializer(fn) Callback that builds the Engine from env.DSN().
Optional options
Option Purpose
WithDatabase(cfg) Database name, credentials, image, startup timeout.
WithCacheInvalidator(fn) Flush ORM-level caches before every reset.
WithSeeders(fns...) Append per-test seeders. Repeatable.
WithPristineDumpPath(p) Override the location of the pristine dump file.
WithTestdataDir(p) Override the testdata directory.
WithSQLitePath(p) Override the SQLite database file path (SQLite only).
WithProjectRoot(p) Bypass automatic go.mod lookup.
WithGeneratePristine(b) Force the slow path on the next setup (regenerate the dump).
WithLogger(l) Replace the default logger. Pass nil to disable output.
Defaults

If WithDatabase is omitted, sensible defaults are applied per driver:

Driver Database Username Password Image
MySQL testdb root testpass mysql:lts
Postgres testdb postgres testpass postgres:16-alpine
SQLite n/a n/a n/a n/a

If WithTestdataDir is omitted, it defaults to <projectRoot>/testdata.

If WithPristineDumpPath is omitted, it defaults to <testdataDir>/<driver>-pristine.sql.

If WithSQLitePath is omitted, it defaults to <testdataDir>/dbtestkit.sqlite.


The Environment type

Every user callback receives an *dbtestkit.Environment. It exposes:

Method Description
Driver() The active Driver constant.
Database() The DatabaseConfig (credentials, image, etc.).
DSN() The connection string. Populated after Start().
ProjectRoot() Absolute path to the project root.
TestdataDir() Absolute path to the testdata directory.
SQLitePath() SQLite file path (SQLite only).
Engine() The user-supplied Engine. Nil during SchemaInitializer (the engine is constructed after schema sync). Non-nil during DataInitializer, CacheInvalidator, and Seeder.
Logger() The active Logger (may be nil).
Context() The lifecycle context (cancelled when the test process exits).

The Engine interface

dbtestkit does not import any specific ORM. Instead, it defines a minimal Engine interface:

type Engine interface {
    Exec(query string, args ...any) (sql.Result, error)
    QueryString(query string, args ...any) ([]map[string]string, error)
    Ping() error
    Close() error
    ClearCache() error
}

If you use xorm, the xormadapter subpackage provides a drop-in:

import "github.com/seyallius/gopherbox/dbtestkit/xormadapter"

eng, _ := xorm.NewEngine("mysql", dsn)
return xormadapter.New(eng), nil

For other ORMs (gorm, sqlx, plain *sql.DB), implement the five methods yourself — it's about 30 lines of boilerplate.


Drivers

MySQL (drivers/mysql)
  • Boots mysql:lts (override via WithDatabase).
  • Uses tmpfs at /var/lib/mysql for in-memory storage.
  • Ships an embedded mysql-quickstart-entrypoint.sh that re-hydrates a pre-baked empty DB tarball (skipping the multi-second initdb first-run sequence).
  • Dump format: mysqldump --add-drop-table --complete-insert --skip-triggers.
  • Reset: mysql < dump.sql (drop + recreate in one pass — no separate TRUNCATE needed).

To generate the empty-DB tarball for the first time, run this once on a machine with Docker:

docker run --rm -v "$PWD/testdata:/out" alpine sh -c '
  apk add --no-cache docker-cli
  docker run -d --name mysql-tmp \
    -e MYSQL_DATABASE=testdb \
    -e MYSQL_ROOT_PASSWORD=testpass \
    mysql:lts
  sleep 30
  docker exec mysql-tmp sh -c "tar cf - -C /var/lib/mysql . | gzip --fast" > /out/empty-mysql.tar.gz
  docker rm -f mysql-tmp
'

If the tarball is missing, the entrypoint gracefully falls back to the official initdb flow.

Postgres (drivers/postgres)
  • Boots postgres:16-alpine (override via WithDatabase).
  • Uses tmpfs at /var/lib/postgresql/data.
  • Dump format: pg_dump --clean --if-exists --no-owner --no-privileges.
  • Reset: psql -f dump.sql (DROP + CREATE in one pass).

Postgres doesn't ship a quickstart tarball equivalent of MySQL's empty-mysql.tar.gz — initdb runs in ~1 s on tmpfs, which is already fast enough.

SQLite (drivers/sqlite)
  • No Docker. Uses an in-process, file-backed SQLite database.
  • Dump format: binary file snapshot (not a SQL text dump) — dramatically faster than re-running .dump output.
  • Reset: copy the snapshot file over the working DB file (~μs).
  • Truncate fallback: walks sqlite_master and issues DELETE FROM <table> for each user table (used only if no snapshot exists).

Adding a new driver

Implement the dbtestkit.DatabaseDriver interface:

type DatabaseDriver interface {
    Driver() Driver
    Start(ctx context.Context, env *Environment) (string, error)
    RestoreDump(ctx context.Context, env *Environment, dumpPath string) error
    GenerateDump(ctx context.Context, env *Environment, dumpPath string) error
    Truncate(ctx context.Context, env *Environment) error
    Stop(ctx context.Context, env *Environment) error
}

Then expose a New() constructor and pass it to WithDriver:

package redis

import "github.com/seyallius/gopherbox/dbtestkit"

func New() dbtestkit.DatabaseDriver { return &Driver{} }

type Driver struct{ /* ... */ }

func (d *Driver) Driver() dbtestkit.Driver { return dbtestkit.Driver("redis") }
// ... implement the remaining methods ...

Note that dbtestkit.Driver is just a string, so you can use a custom value — but the library's IsValid() predicate won't recognize it. That's fine for in-tree drivers; just don't expect the SupportedDrivers() list to include it.


Caching and reset correctness

The hardest part of test isolation is cache coherence, not database state. The original casdoor code had subtle bugs where Ristretto's async eviction goroutines would hand out stale pointers after a reset. dbtestkit addresses this with two layers:

  1. WithCacheInvalidator callback — invoked before the dump restore. Use this to flush any in-memory stores that hold pointers to DB rows (Ristretto, sync.Map, custom lookup tables).
  2. Engine.ClearCache() — invoked after the cache invalidator. For xorm, this forwards to xorm.Engine.ClearCache() which flushes the ORM-level Ristretto store.

If your ORM has no cache (plain *sql.DB), return nil from both — the library handles it gracefully.


Regenerating the pristine dump

When your schema changes (new table, new column, new default row), regenerate the pristine dump:

// In a one-off test or a makefile target:
dbtestkit.Run(m,
    dbtestkit.WithDriver(mysql.New()),
    dbtestkit.WithGeneratePristine(true), // <-- forces slow path
    // ... rest of options ...
)

Or expose a make pristine target:

.PHONY: pristine
pristine:
	go test -run '^$$' ./... -tags=pristine

The slow path runs once, generates the dump, and saves it to <testdataDir>/<driver>-pristine.sql. Commit the dump to your repo — subsequent test runs use the fast path.


Migrating from the casdoor test utils

The original casdoor code used environment variables (TEST_DB_DRIVER, TEST_GENERATE_PRISTINE). This library replaces them with functional options:

Old (env var) New (option)
TEST_DB_DRIVER=mysql WithDriver(mysql.New())
TEST_DB_DRIVER=sqlite WithDriver(sqlite.New())
TEST_GENERATE_PRISTINE=true WithGeneratePristine(true)

Project-specific functions like object.InitDb(), object.CreateTables(), object.SetupCacheInvalidation() move into the corresponding callbacks:

Old (casdoor) New (dbtestkit callback)
object.CreateTables() WithSchemaInitializer(...)
object.InitDb() WithDataInitializer(...)
object.InitAdapter() + engine setup WithEngineInitializer(...)
object.SetupCacheInvalidation() WithCacheInvalidator(...)
DefaultTestSeeder() WithSeeders(...)

The casdoor-specific MockSession, NewMockBContextAndRecorder, and LogRequestResponse helpers are not part of this library — they're beego-specific and don't belong in a generic DB test toolkit. Copy them into your project's own test utils package.


Design notes

Why functional options instead of env vars?

Env vars are global, hidden, and don't survive refactorings. Functional options are:

  • Discoverable — IDE autocomplete shows every available knob.
  • Composable — multiple WithSeeders calls accumulate; scalar options last-wins.
  • TestableApplyOptionsForTesting lets you assert option application without spinning up a container.
  • Type-safe — passing a non-Driver to WithDriver is a compile error, not a runtime surprise.
Why a DatabaseDriver interface in the core package?

Placing the interface in the core dbtestkit package (rather than a drivers subpackage) avoids an import cycle: driver subpackages import dbtestkit for the Environment type, so dbtestkit cannot import them back. This is the same pattern database/sql uses with driver.Driver.

Why is Reset global?

Go's TestMain model is inherently package-scoped — one process per package. A package-level global is the natural lifecycle boundary. The library serializes resets via a mutex, so t.Parallel() tests share one container safely.

Why does SchemaInitializer see a nil engine?

In the slow path, schema creation (e.g. xorm.Sync2) typically needs its own engine handle. Rather than imposing a specific construction order, the library lets the SchemaInitializer build its own engine from env.DSN() if needed. The "official" engine — the one returned by EngineInitializer — is constructed after schema sync, so env.Engine() is nil during SchemaInitializer and non-nil during DataInitializer, CacheInvalidator, and Seeder.

Why a binary snapshot for SQLite?

SQLite's .dump produces a SQL text file that's expensive to replay (each statement is a separate transaction). Copying the database file is ~100× faster and produces identical state. The trade-off is that the snapshot is binary — you can't inspect or diff it — but for test fixtures that's fine.


Running the tests

The library ships with three categories of tests:

# Unit tests (no Docker required)
go test ./...

# SQLite integration tests (no Docker required)
go test ./drivers/sqlite/...

# MySQL / Postgres integration tests (require Docker)
go test ./drivers/mysql/...
go test ./drivers/postgres/...
SQLite driver used in tests

The SQLite integration tests use modernc.org/sqlite — a pure-Go (no CGO) SQLite driver. This means the tests run out-of-the-box on Windows / macOS / Linux without requiring a C toolchain. The library itself is ORM-agnostic: production users of dbtestkit can choose modernc.org/sqlite, mattn/go-sqlite3 (CGO), or any other SQLite driver — whatever they pass to their WithEngineInitializer is what gets used.


License

MIT — see LICENSE.

Documentation

Overview

Package dbtestkit. doc.go - Provides a generic, ORM-agnostic test container toolkit for integration tests that need a real database (MySQL, PostgreSQL, or SQLite).

dbtestkit is designed to be wired into a Go test package's TestMain function. It boots a database (in Docker for MySQL/PostgreSQL, or on local disk for SQLite), restores a pre-baked pristine SQL dump for millisecond resets between tests, and exposes a single Reset entry-point that returns the database to a known-good state before every test.

Configuration is entirely functional-options based — no environment variables. Every project-specific concern (schema creation, data seeding, engine construction, cache invalidation) is supplied by the caller via callbacks, so the library never imports your ORM or your domain objects.

Package dbtestkit. driver.go - Defines the set of supported database drivers and provides validation helpers.

Package dbtestkit. engine.go - Defines the minimal Engine abstraction the library needs to talk to the user's database for truncation, pings, and cache invalidation.

The library intentionally does NOT depend on any specific ORM (xorm, gorm, sqlx, …). Users adapt their ORM to this interface; an xorm adapter ships in the dbtestkit/xormadapter subpackage.

Package dbtestkit. environment.go - Defines the Environment struct that is passed to every user-supplied callback. It exposes the runtime context the caller needs (DSN, driver, project root, logger, engine handle) without leaking internal state.

Package dbtestkit. lifecycle.go - Orchestrates the test lifecycle. Exposes two public entry points: Run (called from TestMain) and Reset (called from each test). Everything else is internal plumbing.

The library uses a package-scoped global to share state between Run and Reset because Go's TestMain model is inherently package-scoped — the test binary boots one process per package, so a package-level global is the natural lifecycle boundary.

Package dbtestkit. logger.go - Provides the Logger interface and a default tree-style printer that mirrors the original casdoor test output format.

Package dbtestkit. options.go - Defines the functional-options API used to configure a test environment.

All configuration is supplied via WithXxx options — there are no environment variables. This keeps tests hermetic, makes the configuration discoverable via code completion, and avoids global state.

Package dbtestkit. project.go - Provides the helper that locates the project root directory by walking up the filesystem until a go.mod file is found.

Package dbtestkit. testing.go - Exposes a minimal set of internal hooks for use by tests in this package and downstream consumers that need to assert option application without spinning up a real container.

This file ships with the library (it is not a _test.go file) so the functions it exposes are part of the public API. They are clearly named "ForTesting" to discourage production use.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Reset

func Reset[S stringish](t MinimalTandB, testNames ...S) error

Reset restores the database to its pristine state and runs any custom seeders. Call this at the top of every test that depends on a clean database.

testNames is an optional list of human-readable string labels (e.g. sub-test names) that will be echoed in the reset banner for log readability.

func ResetForTesting

func ResetForTesting(
	t MinimalTandB,
	opts ...Option,
) error

ResetForTesting runs the package-internal resetDatabase function against a runtime state assembled from the given options + a previously-returned Environment/Engine/Driver triple. It does NOT call os.Exit and is safe to use from test code.

seeders and cacheInvalidator override whatever was supplied via opts.

func Run

func Run(
	m RunM,
	driver DatabaseDriver,
	schemaInit SchemaInitializer,
	dataInit DataInitializer,
	engineInit EngineInitializer,
	opts ...Option,
)

Run is the entry point called from TestMain. It boots the driver, runs the slow or fast setup path, executes the test suite, and tears everything down. On any setup error it prints the error and calls os.Exit(1) — TestMain has no other way to fail before tests run.

driver, schemaInit, dataInit, and engineInit are the four pieces of configuration every caller must supply — there is no meaningful default for "which database" or "how do I create your schema." Making them positional means a TestMain that omits one fails to compile, instead of failing on the first `go test` run with a "dbtestkit: WithXxx is required" string. Everything else (credentials, paths, seeders, logger, cache invalidation) is genuinely optional and stays in opts.

Example:

dbtestkit.Run(m, mysql.New(), tests.CasdoorSchemaInit, tests.CasdoorDataInit, tests.CasdoorEngineInit,
    dbtestkit.WithDatabase(dbtestkit.DatabaseConfig{Database: "casdoor", Username: "root", Password: "casdoor"}),
    dbtestkit.WithCacheInvalidator(tests.CasdoorCacheInvalidator),
    dbtestkit.WithSeeders(seedFn),
)

If you already have call sites built against the old all-options form, they keep compiling unchanged: WithDriver / WithSchemaInitializer / WithDataInitializer / WithEngineInitializer are still accepted and simply overwrite the same fields (last write wins, same as any other Option).

func SetupForTesting

func SetupForTesting(opts ...Option) (*Environment, Engine, DatabaseDriver, error)

SetupForTesting runs the package-internal setup function with a config derived from the given options. It returns the constructed Environment, Engine, and the resolved DatabaseDriver, exactly as the production Run function would see them.

Tests use this to exercise the fast/slow-path logic without invoking os.Exit (which Run does and which makes direct testing impossible).

Types

type CacheInvalidator

type CacheInvalidator func(env *Environment) error

CacheInvalidator is invoked before every reset to flush ORM-level caches. Return nil if your ORM has no cache.

type DataInitializer

type DataInitializer func(env *Environment) error

DataInitializer is invoked once on the slow path to seed base data.

type DatabaseConfig

type DatabaseConfig struct {
	// Database is the name of the database to create inside the container.
	Database string

	// Username is the database user the engine will connect as.
	Username string

	// Password is the database user's password.
	Password string

	// Image is the Docker image reference, e.g. "mysql:lts" or "postgres:16-alpine".
	Image string

	// StartupTimeout is the maximum time to wait for the container to become
	// ready. Zero means use the driver default.
	StartupTimeout time.Duration
}

DatabaseConfig holds the credentials and image selection for a containerized database. Fields not relevant to a given driver (e.g. Image for SQLite) are ignored.

type DatabaseDriver

type DatabaseDriver interface {
	Driver() Driver
	Start(ctx context.Context, env *Environment) (string, error)
	RestoreDump(ctx context.Context, env *Environment, dumpPath string) error
	GenerateDump(ctx context.Context, env *Environment, dumpPath string) error
	Truncate(ctx context.Context, env *Environment) error
	Stop(ctx context.Context, env *Environment) error
	ResetStrategy() ResetStrategy
}

type DefaultLogger

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

DefaultLogger prints tree-style timing output to stdout, matching the format used by the original casdoor test utilities:

===> 🚀 Test Container Setup
    ├── Create MySQL Container:                420ms
    └── ✅ Test Container Ready!               1.2s

func NewDefaultLogger

func NewDefaultLogger(w io.Writer) *DefaultLogger

NewDefaultLogger returns a DefaultLogger writing to w. Pass nil to use os.Stdout.

func (*DefaultLogger) End

func (l *DefaultLogger) End(name string, elapsed time.Duration)

End implements Logger.

func (*DefaultLogger) Info

func (l *DefaultLogger) Info(msg string)

Info implements Logger.

func (*DefaultLogger) Step

func (l *DefaultLogger) Step(name string, elapsed time.Duration)

Step implements Logger.

func (*DefaultLogger) Warn

func (l *DefaultLogger) Warn(msg string)

Warn implements Logger.

type Driver

type Driver string

Driver identifies a supported database backend.

Use one of the named constants below when configuring the environment via WithDriver. Custom drivers can be registered separately via the drivers subpackage.

const (
	// DriverMySQL selects a MySQL 8.x test container.
	DriverMySQL Driver = "mysql"

	// DriverPostgres selects a PostgreSQL test container.
	DriverPostgres Driver = "postgres"

	// DriverSQLite selects an in-process, file-backed SQLite database (no Docker).
	DriverSQLite Driver = "sqlite"
)

func SupportedDrivers

func SupportedDrivers() []Driver

SupportedDrivers returns the list of drivers built into the library.

Useful for documentation, CLI flag validation, or generating help text.

func (Driver) IsValid

func (d Driver) IsValid() bool

IsValid reports whether the given driver is supported by the library.

func (Driver) String

func (d Driver) String() string

String implements fmt.Stringer.

type Engine

type Engine interface {
	// Exec executes a statement that does not return rows.
	Exec(query string, args ...any) (sql.Result, error)

	// QueryString runs a query and returns each row as a map[string]string
	// (column name → stringified value). Used for SHOW TABLES / sqlite_master.
	QueryString(query string, args ...any) ([]map[string]string, error)

	// Ping verifies the database connection is alive.
	Ping() error

	// Close releases the underlying connection pool.
	Close() error

	// ClearCache flushes any ORM-level in-memory cache.
	//
	// Return nil if the underlying ORM has no cache (e.g. plain *sql.DB).
	ClearCache() error
}

Engine is the minimal database surface area the library requires.

It exists so the library can TRUNCATE tables, ping the DB, clear ORM-level caches, and close the connection — without importing any ORM. Implementations only need to forward these calls to the underlying *sql.DB, *xorm.Engine, *gorm.DB, etc.

type EngineInitializer

type EngineInitializer func(env *Environment) (Engine, error)

EngineInitializer constructs the user's Engine from the DSN exposed on env. Called once per package setup; the returned Engine is reused for all tests.

type Environment

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

Environment is the runtime handle passed to every user callback (schema init, data init, engine init, cache invalidation, seeders).

It is intentionally read-only from the caller's perspective. The library populates it during the setup phase; callers consume its fields.

func EnvWithEngineForTesting

func EnvWithEngineForTesting(env *Environment, eng Engine) *Environment

EnvWithEngineForTesting returns a shallow copy of env with the Engine field replaced. Used by driver tests to inject a test-specific Engine before calling Truncate.

func NewEnvironmentForTesting

func NewEnvironmentForTesting(base *Environment, driverName Driver, testdataDir, sqlitePath string) *Environment

NewEnvironmentForTesting constructs an Environment with the minimal fields a driver needs to operate (driver name, testdata dir, sqlite path).

If base is non-nil, its other fields (DSN, project root, logger) are inherited; otherwise sensible defaults are used. This is intended for driver-level tests that want to exercise Start/Stop/RestoreDump/Truncate in isolation without invoking the full lifecycle.

func (*Environment) Context

func (e *Environment) Context() context.Context

Context returns the lifecycle context bound to the current setup or reset operation. Callers should prefer this context over context.Background() so their work is cancelled when the test process exits.

func (*Environment) DSN

func (e *Environment) DSN() string

DSN returns the connection string the engine should use to connect.

For MySQL/Postgres this is a network DSN; for SQLite this is a file:// URL. The value is only populated after the underlying container (or file) has been created.

func (*Environment) Database

func (e *Environment) Database() DatabaseConfig

Database returns the database credentials and image configuration supplied via WithDatabase (or the driver's defaults if WithDatabase was not called).

Driver implementations use this to build container requests. User callbacks typically do not need it — they should use DSN() instead.

func (*Environment) Driver

func (e *Environment) Driver() Driver

Driver returns the active database driver.

func (*Environment) DriverConfig

func (e *Environment) DriverConfig() DatabaseConfig

DriverConfig is an alias for Database used by driver implementations. Kept as a separate method so the public API reads naturally for both audiences.

func (*Environment) Engine

func (e *Environment) Engine() Engine

Engine returns the user-supplied Engine, or nil if it has not been constructed yet (e.g. during the schema-initialization phase).

func (*Environment) Logger

func (e *Environment) Logger() Logger

Logger returns the active Logger.

func (*Environment) ProjectRoot

func (e *Environment) ProjectRoot() string

ProjectRoot returns the absolute path to the project root (where go.mod lives).

func (*Environment) SQLitePath

func (e *Environment) SQLitePath() string

SQLitePath returns the on-disk location of the SQLite database file.

Only meaningful when Driver() == DriverSQLite. Returns the empty string for other drivers.

func (*Environment) TestdataDir

func (e *Environment) TestdataDir() string

TestdataDir returns the absolute path to the testdata directory.

Defaults to <projectRoot>/testdata. Override with WithTestdataDir.

type Logger

type Logger interface {
	// Step reports the elapsed time of an inner setup/reset step.
	Step(name string, elapsed time.Duration)

	// End reports the elapsed time of an outer setup/reset phase.
	End(name string, elapsed time.Duration)

	// Info emits an informational line (e.g. "==> 🚀 Test Container Setup").
	Info(msg string)

	// Warn emits a non-fatal warning.
	Warn(msg string)
}

Logger is the logging surface used by the library. Implementations are free to forward to *testing.T, log/slog, zap, or any other sink.

type MinimalTandB

type MinimalTandB interface {
	Helper()
	Logf(format string, args ...any)
	Errorf(format string, args ...any)
	Fatalf(format string, args ...any)
}

MinimalTandB is the subset of *testing.T / *testing.B the library needs. Defined here so callers can pass either without the library depending on the testing package at compile time (other than in tests).

type NoopEngine

type NoopEngine struct{}

NoopEngine is a no-op Engine implementation intended for tests that want to exercise library plumbing without a real database. Production callers should provide a real engine via WithEngineInitializer.

func (NoopEngine) ClearCache

func (NoopEngine) ClearCache() error

ClearCache satisfies the Engine interface.

func (NoopEngine) Close

func (NoopEngine) Close() error

Close satisfies the Engine interface.

func (NoopEngine) Exec

func (NoopEngine) Exec(string, ...any) (sql.Result, error)

Exec satisfies the Engine interface and returns a no-op result.

func (NoopEngine) Ping

func (NoopEngine) Ping() error

Ping satisfies the Engine interface.

func (NoopEngine) QueryString

func (NoopEngine) QueryString(string, ...any) ([]map[string]string, error)

QueryString satisfies the Engine interface and returns an empty slice.

type Option

type Option func(*config) error

Option configures the test environment.

func WithCacheInvalidator

func WithCacheInvalidator(fn CacheInvalidator) Option

WithCacheInvalidator supplies an optional callback invoked before every reset.

Use this to flush ORM-level caches (e.g. xorm's Ristretto store) so that post-reset reads do not return stale pointers from the previous test.

func WithDataInitializer deprecated

func WithDataInitializer(fn DataInitializer) Option

WithDataInitializer supplies the callback that seeds base data on the slow path.

Deprecated: pass it as Run's fourth positional argument instead. Kept for backward compatibility.

func WithDatabase

func WithDatabase(db DatabaseConfig) Option

WithDatabase configures credentials and image for the containerized database. Required for MySQL and Postgres; ignored for SQLite.

func WithDriver deprecated

func WithDriver(d DatabaseDriver) Option

WithDriver supplies the database driver implementation.

Deprecated: pass the driver as the second positional argument to Run instead — that form is checked by the compiler, so a missing driver is a build error instead of a runtime "WithDriver is required" failure. This option is kept only so existing call sites continue to compile; the last value set (whether by this option or by Run's positional argument) wins.

Pass the constructor result from the desired driver subpackage:

import "github.com/seyallius/gopherbox/dbtestkit/drivers/mysql"

dbtestkit.Run(m, mysql.New(), schemaInit, dataInit, engineInit,
    dbtestkit.WithDatabase(dbCfg),
)

func WithEngineInitializer deprecated

func WithEngineInitializer(fn EngineInitializer) Option

WithEngineInitializer supplies the callback that builds the Engine from the DSN exposed on the Environment.

Deprecated: pass it as Run's fifth positional argument instead. Kept for backward compatibility.

func WithGeneratePristine

func WithGeneratePristine(force bool) Option

WithGeneratePristine forces the slow path on the next setup, regenerating the pristine dump file even if one already exists. Useful as a one-shot command (e.g. invoked by a `make pristine` target).

func WithLogger

func WithLogger(l Logger) Option

WithLogger replaces the default logger. Pass nil to disable output entirely.

func WithPristineDumpPath

func WithPristineDumpPath(path string) Option

WithPristineDumpPath overrides the location of the pristine SQL dump file.

Defaults to <testdataDir>/<driver>-pristine.sql.

func WithProjectRoot

func WithProjectRoot(path string) Option

WithProjectRoot explicitly sets the project root directory, bypassing the automatic go.mod lookup. Useful in environments where the working directory is not inside the module tree.

func WithSQLitePath

func WithSQLitePath(path string) Option

WithSQLitePath overrides the on-disk location of the SQLite database file. SQLite driver only. Defaults to <testdataDir>/dbtestkit.sqlite.

func WithSchemaInitializer deprecated

func WithSchemaInitializer(fn SchemaInitializer) Option

WithSchemaInitializer supplies the callback that creates tables.

Deprecated: pass it as Run's third positional argument instead; see WithDriver's doc comment for why. Kept for backward compatibility.

For xorm users this is typically engine.Sync2(&MyModel{}).

func WithSeeders

func WithSeeders(seeders ...Seeder) Option

WithSeeders appends custom seeder functions executed after the pristine state is restored on every reset. Repeatable; later calls append.

func WithTestdataDir

func WithTestdataDir(dir string) Option

WithTestdataDir overrides the testdata directory used to store auxiliary files (pristine dumps, pre-baked container tarballs, etc.).

Defaults to <projectRoot>/testdata.

type ResetStrategy added in v0.3.2

type ResetStrategy int

ResetStrategy dictates how the database is restored to a pristine state between tests.

const (
	// ResetStrategyRestoreDump uses the driver's RestoreDump method (e.g., piping SQL or copying files).
	// This is the fastest approach for network databases like MySQL/Postgres.
	ResetStrategyRestoreDump ResetStrategy = iota

	// ResetStrategyTruncateAndSeed truncates all tables and re-runs the DataInitializer.
	// This is necessary for in-process databases like SQLite on Windows, where file locks
	// prevent replacing the database file while connections are open.
	ResetStrategyTruncateAndSeed
)

type RunM

type RunM interface {
	Run() int
}

RunM is the subset of *testing.M the library needs. Defined here so the core package does not need to import "testing" at compile time (other than in test files).

type SchemaInitializer

type SchemaInitializer func(env *Environment) error

SchemaInitializer is invoked once on the slow path to create tables. It receives the live Environment (engine may be nil if the user chose to construct the engine after schema sync — see WithEngineInitializer).

type Seeder

type Seeder func(env *Environment) error

Seeder is invoked after the pristine state has been restored, on every reset. Use it to insert test-specific fixtures.

type TestingConfig

type TestingConfig struct {
	DriverName       Driver
	Database         DatabaseConfig
	SchemaInit       SchemaInitializer
	DataInit         DataInitializer
	EngineInit       EngineInitializer
	CacheInvalidator CacheInvalidator
	Seeders          []Seeder
	PristineDumpPath string
	TestdataDir      string
	SQLitePath       string
	ProjectRoot      string
	GeneratePristine bool
	Logger           Logger
}

TestingConfig is a read-only view of the internal config struct, exposed for tests that need to assert what options were applied. Production code should never use this type.

func ApplyOptionsForTesting

func ApplyOptionsForTesting(opts ...Option) (*TestingConfig, error)

ApplyOptionsForTesting builds a TestingConfig from a list of options.

This is the only sanctioned way for tests to inspect option application without reimplementing the validation logic. Production callers should use Run, which performs the same validation internally.

Returns an error if the options fail validation (missing required fields, nil callbacks, etc.).

Directories

Path Synopsis
drivers
mysql
Package mysql.
Package mysql.
postgres
Package postgres.
Package postgres.
sqlite
Package sqlite.
Package sqlite.
Package xormadapter.
Package xormadapter.

Jump to

Keyboard shortcuts

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