testdb

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

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

Go to latest
Published: Jun 16, 2023 License: MIT Imports: 12 Imported by: 0

README

testdb

Go Reference

Facilitates testing for applications that use a database in Golang. This module takes the approach of using a real database, rather than mocking them out.

Generally this is geared-towards relational/SQL databases which use migrations to define their schema. Only Postgres handling is provided out of the box for now, but Initializer can be implemented to provide initialisation logic for other vendors.

A test using this module may look something like this:

package user_tests

import (
	"github.com/vaeryn-uk/go-testdb"
	"testing"
)

func TestUserLoggedIn(t *testing.T) {
	// Initialise a database. If you're using this in many tests, extract this to a helper function.
	// Note this database is removed at the end of the test automatically.
	testDb := testdb.NewPg(
		t,
		"postgresql://db/test_db?user=testuser&password=testpassword",
		testdb.CliMigrator(t, "/path/to/migrations"),
	)

	// Insert some data in to the database.
	testDb.Insert(t, "users", map[string]any{"id": 12345, "username": "Scotty"})

	// Your test code goes here, e.g. logging the user in.

	// Afterwards, you may inspect the state of the database.
	var actual bool
	testDb.QueryValue(t, "SELECT has_logged_in FROM users WHERE id = %s", &actual, 12345)

	if !actual {
		t.Fatalf("expected user 12345 to have been logged in, but they were not")
	}
}

Given the potential time costs of migrating a database per test, this module applies migrations to templates and then creates databases from those templates. Support is included to intelligently detect for changes to migrations. In practice, this means that we rarely need to do the application migrations, cutting down on test time.

This module was born from personal projects. Feel free to use this, but consider it experimental. Docker is a useful tool for running databases in both dev & CI environments to not have to worry about setup of a database server across multiple environments.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var DefaultOptions = Options{
	TemplateNameStr: "test_template_%s",
	DatabaseNameStr: "test_db_%s",
}

The DefaultOptions when initializing a test database.

View Source
var ErrorHandler = func(t testing.TB, err error, extra ...any) {
	t.Helper()

	t.Fatal(append([]any{"testdb initialisation failed", err}, extra...))
}

ErrorHandler will be invoked for any error throughout testdb initialisation or interaction. This is expected to halt & fail the test immediately. You may override this for custom outputs.

Functions

This section is empty.

Types

type Db

type Db interface {
	// The Name of the test database.
	Name() string
	// Dsn is the data source name of the test database; a connection string.
	Dsn() string
	// Insert will insert the provided data into table within the test database.
	// Each data is expected to be a col => val mapping.
	// For convenience, multiple data entries may be provided; each will be inserted
	// separate as a new row.
	Insert(t testing.TB, table string, data ...map[string]any)
	// QueryValue will runs sql with args, writing a single value back to into. Provide
	// a pointer for into.
	QueryValue(t testing.TB, sql string, into any, args ...any)
	// QueryRow will scan the results of the first row of a query in to the pointer values
	// provided in the returned func. This will fatal the test if there is error scanning
	// in to the provided values, or if there are 0 rows.
	QueryRow(t testing.TB, sql string, args ...any) func(into ...any)
	// Exec applies arbitrary SQL commands on this test database. If the command produces
	// an error, the test will error.
	Exec(t testing.TB, sql string, args ...any) ExecResult
	// Drop forcefully removes the database. This is automatically done as part of database
	// cleanup.
	Drop(t testing.TB)
}

Db is the core handle provided to your tests. This represents a fully migrated, test database ready for use. This database is always brand new, so has isolation from other tests. Use New or its variants to get one of these.

func New

func New[Conn any](t testing.TB, dsn string, h Initializer[Conn], m Migrator, options Options) Db

New initialises a new test database at the database indicated by dsn. dsn must be a valid connection that has permission to create new databases. Returns the Db handle representing a fully migrated, isolated database ready for use in your test. If the provided m is nil, no migrations will be applied. Instead, a blank database will be created.

You may want to use a ready-provided constructor, such as NewPg. This is exposed for custom initializers if you're using a database that isn't supported.

func NewPg

func NewPg(t testing.TB, dsn string, migrator Migrator, opts Options) Db

NewPg initialises a new Postgres test database at the database indicated by dsn. dsn must be a valid connection that has permission to create new databases. Returns the Db handle representing a fully migrated, isolated database ready for use in your test.

provide a nil migrator to disable any migrations and return a blank database instead.

type ExecResult

type ExecResult struct {
	RowsAffected int64
}

type Initializer

type Initializer[Conn any] interface {
	// Connect returns an active connection to the provided DSN.
	Connect(t testing.TB, dsn string) Conn

	// Lock provides some protection against concurrent migration application for parallel
	// tests. This should ensure that only one testdb initialization is happening. Should
	// lock against the provided name. This may be done with stdlib sync stuff, or better
	// yet, at the database itself if possible.
	// This will block until the lock is acquired.
	Lock(t testing.TB, conn Conn, name string)

	// Unlock releases the lock acquired in Lock.
	Unlock(t testing.TB, conn Conn, name string)

	// Exists checks if the database with name exists in the database already.
	Exists(t testing.TB, conn Conn, name string) bool

	// Create a new blank database with the given name.
	Create(t testing.TB, conn Conn, name string)

	// CreateFromTemplate creates a new database with name, using a template database
	// with name template.
	CreateFromTemplate(t testing.TB, conn Conn, template, name string)

	// NewDsn takes a base DSN and returns a new one with the given newName
	// as the database name. This will override the dbname portion of dsn
	// with the new name.
	NewDsn(t testing.TB, base string, newName string) string

	// NewDb creates a new Db. rootDsn is the connection to the database
	// used to manage database creation etc.; dsn is the connection string
	// for the newly-created test database.
	NewDb(t testing.TB, rootDsn, dsn string) Db

	// Remove removes the database given by name entirely using the provided
	// root connection.
	Remove(t testing.TB, conn Conn, name string)

	// Close a connection once we're done with.
	Close(conn Conn)
}

type Migrator

type Migrator interface {
	// Hash the current state of migrations. This should return a different value
	// any time the migration definitions are changed. Commonly, this involves hashing
	// the contents of a migrations directory.
	Hash(t testing.TB) string
	// Migrate applies the current migrations to the provided dsn (data source name).
	Migrate(t testing.TB, dsn string)
}

Migrator is responsible for applying migrations to a database.

func CliMigrator

func CliMigrator(t testing.TB, dir string) Migrator

CliMigrator implements a migration strategy that uses the command line (linux only). Assumes migrate is installed: https://github.com/golang-migrate/migrate#cli-usage

type Options

type Options struct {
	// TemplateNameStr is used when creating template databases. You can use this to
	// customise the name of template databases that are created. This must include
	// %s which will be replaced by a unique ID generated by this package.
	TemplateNameStr string
	// DatabaseNameStr is used when creating test databases. You can use this to
	// customise the name of the databases used for tests. This must include
	// %s which will be replaced by a unique ID generated by this package.
	DatabaseNameStr string
}

Options allows for customization of how test database are initialized. Use DefaultOptions if you don't need to customize.

type PgDb

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

func (*PgDb) Drop

func (p *PgDb) Drop(t testing.TB)

func (*PgDb) Dsn

func (p *PgDb) Dsn() string

func (*PgDb) Exec

func (p *PgDb) Exec(t testing.TB, sql string, args ...any) ExecResult

func (*PgDb) Insert

func (p *PgDb) Insert(t testing.TB, table string, data ...map[string]any)

func (*PgDb) Name

func (p *PgDb) Name() string

func (*PgDb) QueryRow

func (p *PgDb) QueryRow(t testing.TB, sql string, args ...any) func(into ...any)

func (*PgDb) QueryValue

func (p *PgDb) QueryValue(t testing.TB, sql string, into any, args ...any)

Jump to

Keyboard shortcuts

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