mysql

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Oct 22, 2026 License: MIT Imports: 20 Imported by: 0

Documentation

Overview

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.

import (
    "github.com/bashhack/testdb"
    "github.com/bashhack/testdb/migrate/goose"
    "github.com/bashhack/testdb/mysql"
)

func TestOrders(t *testing.T) {
    db := mysql.Setup(t, testdb.WithMigrator(goose.Dir("./migrations")))
    // db is a *sql.DB on a database nobody else touches; cleanup is automatic
}

The admin connection string is discovered from TEST_MYSQL_URL, then TEST_DATABASE_URL, then DATABASE_URL, falling back to DefaultAdminDSN. It may be in go-sql-driver's native form (user:pass@tcp(host:port)/db?params) or a mysql://user:pass@host:port/db?params URL. The user needs CREATE and DROP DATABASE and the right to kill other sessions (CONNECTION_ADMIN).

Templates (EnsureTemplate) build the schema once and hand each test a clone prepared ahead of time, which is where MySQL's speed comes from; see Template and WithReadyDatabases.

Example (CustomInitializer)

Example_customInitializer hands the database to your own type, for sqlx, GORM, or an application wrapper.

package main

import (
	"context"
	"database/sql"
	"fmt"
	"testing"

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

func main() {
	var t testing.TB

	db := mysql.New(t, &appDBInitializer{},
		testdb.WithMigrator(mysql.LoadSQLFile("./schema.sql")))

	appDB := db.Entity().(*appDB)
	fmt.Println(appDB.Name)
}

// appDB stands in for an application's own database type.
type appDB struct {
	*sql.DB
	Name string
}

type appDBInitializer struct{}

func (appDBInitializer) InitializeTestDatabase(ctx context.Context, dsn string) (any, error) {
	db, err := sql.Open("mysql", dsn)
	if err != nil {
		return nil, err
	}
	return &appDB{DB: db, Name: "app"}, nil
}
Example (KeepOnFailure)

Example_keepOnFailure keeps a failed test's database and logs a mysql command line to open it.

package main

import (
	"testing"

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

func main() {
	var t testing.TB

	db := mysql.Setup(t, testdb.WithKeepOnFailure())
	_ = db
	// On failure the test log shows:
	//   testdb: kept database test_1761100000_a1b2c3d4 for inspection:
	//       mysql --host='localhost' --port=3306 --user='root' --password='root' 'test_1761100000_a1b2c3d4'
	// The same switch for a whole run: TESTDB_KEEP_ON_FAILURE=1 go test ./...
}
Example (Setup)

Example_setup is the common case: an isolated database with the schema applied, cleaned up when the test ends.

package main

import (
	"context"
	"testing"

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

func main() {
	var t testing.TB // in a test this is your *testing.T

	db := mysql.Setup(t, testdb.WithMigrator(mysql.SQLFiles("./schema")))

	_, err := db.ExecContext(context.Background(),
		"INSERT INTO users (email) VALUES (?)", "test@example.com")
	if err != nil {
		t.Fatalf("Insert failed: %v", err)
	}
	// No cleanup to write: the database is dropped via t.Cleanup
}
Example (Template)

Example_template builds the schema once in TestMain and gives every test a prepared clone, which is where MySQL's speed comes from.

package main

import (
	"context"
	"log"
	"os"
	"testing"

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

func main() {
	// func TestMain(m *testing.M) {
	ctx := context.Background()

	tpl, err := mysql.EnsureTemplate(ctx, "app_schema",
		mysql.SQLFiles("./schema"),
		mysql.WithReadyDatabases(8)) // clones kept prepared ahead of tests
	if err != nil {
		log.Fatal(err)
	}

	var m *testing.M
	code := m.Run()

	if err := tpl.Release(ctx); err != nil {
		log.Print(err)
	}
	os.Exit(code)
	// }
	//
	// func TestOrders(t *testing.T) {
	//     db := mysql.Setup(t, testdb.WithTemplate("app_schema")) // ~6 ms
	// }
}

Index

Examples

Constants

View Source
const (
	// AdminDSNEnv is the environment variable this provider checks first for
	// its admin connection string, before the shared TEST_DATABASE_URL and
	// DATABASE_URL. Set it when a project tests against more than one
	// database server.
	AdminDSNEnv = "TEST_MYSQL_URL"

	// DefaultAdminDSN is used when no override or environment variable is set.
	DefaultAdminDSN = "root:root@tcp(localhost:3306)/?parseTime=true"
)

Variables

This section is empty.

Functions

func LoadSQLFile

func LoadSQLFile(path string) testdb.MigratorFunc

LoadSQLFile returns a migrator that runs one SQL file against the database, for schema dumps and fixture data. The file may hold several statements. Client-only syntax such as the mysql CLI's DELIMITER command is not SQL and will not work.

mysql.Setup(t, testdb.WithMigrator(mysql.LoadSQLFile("schema.sql")))

func New

func New(t testing.TB, initializer testdb.DBInitializer, opts ...testdb.Option) *testdb.TestDatabase

New creates an isolated MySQL database with a custom initializer (sqlx, GORM, your own wrapper) and returns the TestDatabase. Cleanup is registered with t.Cleanup. Calls t.Fatal on any error.

db := mysql.New(t, &SqlxInitializer{}, testdb.WithMigrator(goose.Dir("./migrations")))
sqlxDB := db.Entity().(*sqlx.DB)

func SQLFiles

func SQLFiles(dir string) testdb.MigratorFunc

SQLFiles returns a migrator that runs every .sql file in dir, in lexical order, against the database: the simplest way to apply a schema without a migration tool. Name files so they sort the way they should run (001_users.sql, 002_orders.sql). The directory is read when the migrator runs, not when it is created.

mysql.Setup(t, testdb.WithMigrator(mysql.SQLFiles("./schema")))

func Setup

func Setup(t testing.TB, opts ...testdb.Option) *sql.DB

Setup creates an isolated MySQL database, runs the configured migrator, and returns a *sql.DB on it. Cleanup is registered with t.Cleanup; do not close the returned *sql.DB yourself. Calls t.Fatal on any error.

db := mysql.Setup(t, testdb.WithMigrator(goose.Dir("./migrations")))

func TemplateExists

func TemplateExists(ctx context.Context, name string, opts ...TemplateOption) (bool, error)

TemplateExists reports whether a database with the given name exists. This is a debugging aid; EnsureTemplate handles existence itself.

Types

type Provider

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

Provider implements testdb.Provider for MySQL. It does not implement testdb.TemplateProvider: MySQL has no way to clone one database from another, so WithTemplate is refused with testdb.ErrTemplatesNotSupported.

func (*Provider) BuildDSN

func (p *Provider) BuildDSN(dbName string) (string, error)

BuildDSN returns a DSN for the given database that carries every setting of the admin DSN (credentials, address, parseTime, TLS, timeouts).

func (*Provider) Cleanup

func (p *Provider) Cleanup(ctx context.Context) error

Cleanup closes the admin session.

func (*Provider) CreateDatabase

func (p *Provider) CreateDatabase(ctx context.Context, name string) error

CreateDatabase creates an empty database with the server's default character set and collation.

func (*Provider) CreateDatabaseFromTemplate

func (p *Provider) CreateDatabaseFromTemplate(ctx context.Context, requestedName, templateName string) (string, error)

CreateDatabaseFromTemplate hands the test a database cloned from the template: one this process prepared in advance if there is one ready (see WithReadyDatabases), otherwise a clone made now under the requested name. It never waits for the pool.

func (*Provider) DropDatabase

func (p *Provider) DropDatabase(ctx context.Context, name string) error

DropDatabase drops a database if it exists. Unlike PostgreSQL, MySQL does not refuse while other sessions are connected; it waits for the metadata locks they hold. TerminateConnections first, so the wait is short, and lockWaitTimeout so it is never long.

func (*Provider) Initialize

func (p *Provider) Initialize(ctx context.Context, cfg testdb.Config) error

Initialize resolves the admin DSN and opens the admin session.

func (*Provider) InspectCommand

func (p *Provider) InspectCommand(dsn string) string

InspectCommand returns a mysql command line that opens the database, for the kept-database log (see testdb.WithKeepOnFailure).

func (*Provider) ResolvedAdminDSN

func (p *Provider) ResolvedAdminDSN() string

ResolvedAdminDSN returns the admin DSN in use, in native form.

func (*Provider) TemplateExists

func (p *Provider) TemplateExists(ctx context.Context, name string) (bool, error)

TemplateExists reports whether a database with the given name exists.

func (*Provider) TerminateConnections

func (p *Provider) TerminateConnections(ctx context.Context, name string) error

TerminateConnections kills every other session whose current database is the given one. Sessions that use the database only through qualified names (other_db.table) are not found this way; the pools this package creates always connect to the database directly.

type SQLDBInitializer

type SQLDBInitializer struct {
	// ConfigModifier can adjust the pool after it is opened and before it is
	// pinged, for example to change SetMaxOpenConns.
	ConfigModifier func(*sql.DB)
}

SQLDBInitializer is the default initializer: a *sql.DB on the test database with pool settings suited to tests.

func (*SQLDBInitializer) InitializeTestDatabase

func (i *SQLDBInitializer) InitializeTestDatabase(ctx context.Context, dsn string) (any, error)

InitializeTestDatabase opens and pings a *sql.DB for the test database.

type SetupFunc

type SetupFunc = testdb.MigratorFunc

SetupFunc populates a template database; see postgres.SetupFunc. It is testdb.MigratorFunc, so a migration adapter's Migrate method is one.

type Template

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

Template is this process's handle on a template database obtained from EnsureTemplate. It holds one connection to the server for as long as it lives; that connection's id is how other processes know the template is in use. Call Release when the process is done.

func EnsureTemplate

func EnsureTemplate(ctx context.Context, name string, setup SetupFunc, opts ...TemplateOption) (*Template, error)

EnsureTemplate makes sure the named template database exists and marks it as in use by this process, returning a handle to Release when done. Tests then clone it with testdb.WithTemplate(name) or tpl.Option().

It is safe to call from several test packages or CI jobs at once against the same server: the first process to arrive builds the template with setup, the others wait for it and reuse it, and the last one to Release drops it. A process that crashes drops out of the count because its connection is gone. A build that crashed half-way is recognised (its completeness marker is missing) and redone.

func TestMain(m *testing.M) {
    ctx := context.Background()
    tpl, err := mysql.EnsureTemplate(ctx, "schema", goose.Dir("./migrations").Migrate)
    if err != nil {
        log.Fatal(err)
    }
    code := m.Run()
    if err := tpl.Release(ctx); err != nil {
        log.Print(err)
    }
    os.Exit(code)
}

func (*Template) Name

func (t *Template) Name() string

Name returns the template database's name, for use with testdb.WithTemplate.

func (*Template) Option

func (t *Template) Option() testdb.Option

Option returns the testdb.Option that makes a test database a clone of this template, so tests can write mysql.Setup(t, tpl.Option()).

func (*Template) Ready

func (t *Template) Ready() int

Ready reports how many prepared clones are waiting to be taken right now (see WithReadyDatabases); 0 when the pool is disabled or has run dry.

func (*Template) Release

func (t *Template) Release(ctx context.Context) error

Release ends this process's use of the template: prepared databases this process still holds are dropped, and if no other process is using the template (and WithKeepTemplate was not given) the template is dropped too, along with anything left by processes that died. It is safe to call more than once.

type TemplateOption

type TemplateOption func(*templateConfig)

TemplateOption configures EnsureTemplate.

func WithFingerprint

func WithFingerprint(fingerprint string) TemplateOption

WithFingerprint identifies the template's content, typically a hash of the files it is built from. EnsureTemplate reuses an existing template whose fingerprint matches and rebuilds one whose fingerprint differs, so templates can safely outlive a test run (see WithKeepTemplate). Without a fingerprint an existing template is always reused as it is.

func WithKeepTemplate

func WithKeepTemplate() TemplateOption

WithKeepTemplate leaves the template in place when the last process releases it, so the next run can reuse it instead of rebuilding. Pair it with WithFingerprint, otherwise a kept template is never refreshed.

func WithReadyDatabases

func WithReadyDatabases(n int) TemplateOption

WithReadyDatabases sets how many clones of the template this process keeps prepared ahead of demand. Cloning a MySQL database costs hundreds of milliseconds for a real schema (every table is created again); taking a prepared one costs nothing, and the pool refills while tests run. The default is runtime.GOMAXPROCS(0), which is also go test's default -parallel. 0 disables the pool: every test clones on demand.

EnsureTemplate fills the pool once before returning (see WithReadyWarmup), so the first tests find it full. After that it keeps up as long as tests take clones no faster than the workers (see WithReadyWorkers) can make them; the pool size is also the number of back-to-back quick tests it can absorb. When it runs dry, a test clones on demand rather than wait.

func WithReadyPrefix

func WithReadyPrefix(prefix string) TemplateOption

WithReadyPrefix sets the name prefix of prepared databases (default "test", the same as test databases get).

func WithReadyWarmup

func WithReadyWarmup(on bool) TemplateOption

WithReadyWarmup sets whether EnsureTemplate waits for the pool to fill before returning. On by default: it costs one round of parallel clones, once, and means every test from the first finds a prepared database. Turn it off to start tests immediately and let the pool fill alongside.

func WithReadyWorkers

func WithReadyWorkers(n int) TemplateOption

WithReadyWorkers sets how many clones are made in parallel to keep the pool full. The server handles concurrent clones well, so the default is the pool size.

func WithTemplateAdminDSN

func WithTemplateAdminDSN(dsn string) TemplateOption

WithTemplateAdminDSN overrides the admin connection string used to create and manage the template. By default it is discovered as for test databases (TEST_MYSQL_URL, then the shared variables, then the default).

Directories

Path Synopsis
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.

Jump to

Keyboard shortcuts

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