testutil

package module
v0.3.0-alpha.1 Latest Latest
Warning

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

Go to latest
Published: Aug 26, 2026 License: MIT Imports: 13 Imported by: 0

README

Database TestUtil

Testing utilities for the database module, providing an in-memory SQLite test database component and fixture helpers.

Features

  • TestComponent: In-memory SQLite database with full TestComponent lifecycle support
  • State Management: Reset, Snapshot, and Restore capabilities for test isolation
  • Fixture Helpers: Load test data, truncate tables, assert row counts
  • Migration Driver: In-memory golang-migrate driver fake for proving migration orchestration
  • Auto-Migration: Optional model auto-migration on startup
  • Zero Configuration: Works out of the box with sensible defaults

Quick Start

Basic Usage
package mypackage_test

import (
    "testing"
    dbtestutil "github.com/kbukum/gokit/database/testutil"
    "github.com/kbukum/gokit/testutil"
)

func TestMyFeature(t *testing.T) {
    // Create and start test database
    db := dbtestutil.NewComponent()
    testutil.T(t).Setup(db)
    
    // Use the database
    db.DB().Exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
    db.DB().Exec("INSERT INTO users (name) VALUES (?)", "Alice")
    
    // Your test code here...
}
With Auto-Migration
func TestWithModels(t *testing.T) {
    type User struct {
        ID   uint   `gorm:"primarykey"`
        Name string
    }
    
    // Component will auto-migrate the User model on Start()
    db := dbtestutil.NewComponent().WithModels(&User{})
    testutil.T(t).Setup(db)
    
    // Table is ready to use
    db.DB().Create(&User{Name: "Alice"})
}

TestComponent Interface

The database Component implements testutil.TestComponent:

  • Reset(): Clears all data from all tables while preserving schema
  • Snapshot(): Captures current database state (all tables and rows)
  • Restore(snapshot): Restores database to a previous snapshot
State Management Example
func TestWithStateManagement(t *testing.T) {
    db := dbtestutil.NewComponent()
    testutil.T(t).Setup(db)
    
    db.DB().Exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
    db.DB().Exec("INSERT INTO users (name) VALUES (?)", "Alice")
    
    // Capture state
    snapshot := testutil.T(t).Snapshot(db)
    
    // Modify data
    db.DB().Exec("INSERT INTO users (name) VALUES (?)", "Bob")
    
    // Restore to snapshot - Bob is gone, only Alice remains
    testutil.T(t).Restore(db, snapshot)
    
    // Reset completely - all data cleared
    testutil.T(t).Reset(db)
}

Fixture Helpers

Convenient functions for managing test data:

Loading Data
func TestWithFixtures(t *testing.T) {
    db := dbtestutil.NewComponent()
    testutil.T(t).Setup(db)
    
    db.DB().Exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)")
    
    // Load fixture data
    dbtestutil.LoadFixture(db.DB(), "users", []map[string]any{
        {"name": "Alice", "email": "alice@example.com"},
        {"name": "Bob", "email": "bob@example.com"},
    })
    
    // Or use MustLoadFixture to fail test on error
    dbtestutil.MustLoadFixture(t, db.DB(), "users", []map[string]any{
        {"name": "Charlie", "email": "charlie@example.com"},
    })
}
Table Operations
// Check if table exists
if dbtestutil.TableExists(db.DB(), "users") {
    // ...
}

// Get all table names
tables, err := dbtestutil.GetTableNames(db.DB())

// Count rows
count, err := dbtestutil.CountRows(db.DB(), "users")

// Truncate a table
dbtestutil.TruncateTable(db.DB(), "users")

// Truncate all tables
dbtestutil.TruncateAllTables(db.DB())
Assertions
func TestAssertions(t *testing.T) {
    db := dbtestutil.NewComponent()
    testutil.T(t).Setup(db)
    
    db.DB().Exec("CREATE TABLE users (id INTEGER PRIMARY KEY)")
    
    // Assert table is empty
    dbtestutil.AssertTableEmpty(t, db.DB(), "users")
    
    db.DB().Exec("INSERT INTO users (id) VALUES (1)")
    db.DB().Exec("INSERT INTO users (id) VALUES (2)")
    
    // Assert specific row count
    dbtestutil.AssertRowCount(t, db.DB(), "users", 2)
}

Table-Driven Tests with Reset

Use Reset() to ensure test isolation in table-driven tests:

func TestUserCRUD(t *testing.T) {
    db := dbtestutil.NewComponent().WithModels(&User{})
    testutil.T(t).Setup(db)
    
    tests := []struct {
        name string
        fn   func(t *testing.T)
    }{
        {"Create", testCreateUser},
        {"Read", testReadUser},
        {"Update", testUpdateUser},
        {"Delete", testDeleteUser},
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            // Reset to clean state before each test case
            testutil.T(t).Reset(db)
            tt.fn(t)
        })
    }
}

Multiple Snapshots

You can take multiple snapshots and restore to any of them:

func TestMultipleStates(t *testing.T) {
    db := dbtestutil.NewComponent()
    testutil.T(t).Setup(db)
    
    db.DB().Exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
    
    // State 1: Empty
    snap1 := testutil.T(t).Snapshot(db)
    
    // State 2: One user
    db.DB().Exec("INSERT INTO users (name) VALUES (?)", "Alice")
    snap2 := testutil.T(t).Snapshot(db)
    
    // State 3: Two users
    db.DB().Exec("INSERT INTO users (name) VALUES (?)", "Bob")
    snap3 := testutil.T(t).Snapshot(db)
    
    // Jump back to any state
    testutil.T(t).Restore(db, snap2)  // Back to one user
    testutil.T(t).Restore(db, snap1)  // Back to empty
    testutil.T(t).Restore(db, snap3)  // Forward to two users
}

Integration Tests

Combine with other test components for integration testing:

func TestIntegration(t *testing.T) {
    ctx := context.Background()
    manager := testutil.NewManager(ctx)
    
    // Add components
    db := dbtestutil.NewComponent().WithModels(&User{})
    
    manager.Add(db)
    
    // Start all components
    if err := manager.StartAll(); err != nil {
        t.Fatal(err)
    }
    defer manager.Cleanup()
    
    // Run integration tests...
}

Migration Driver

MigrationDriver is an in-memory golang-migrate driver fake for proving migration orchestration (Up/Down/Steps/Reset/Version) without a real database backend. It records the applied version and run count and can be told to fail specific operations, so failure and rollback paths are provable deterministically — reuse it instead of hand-rolling a fake driver per adapter.

func TestMigrations(t *testing.T) {
    driver := dbtestutil.NewMigrationDriver()
    cfg := migration.Config{DB: db, FS: migrationsFS, Path: "migrations", Driver: driver.DriverFunc()}

    if err := cfg.Up(); err != nil {
        t.Fatalf("Up: %v", err)
    }

    // Prove a failing rollback is surfaced, not swallowed.
    driver.FailRun()
    if err := cfg.Down(); err == nil {
        t.Fatal("expected wrapped 'migrate down' error")
    }
}

FailRun, FailSetVersion, and FailDrop inject failures into the corresponding driver operations; Runs reports how many statements have been applied.

Best Practices

1. Use Auto-Migration for Models

When testing with GORM models, use WithModels() to automatically create tables:

// Good ✓
db := dbtestutil.NewComponent().WithModels(&User{}, &Post{})
testutil.T(t).Setup(db)

// Avoid ✗ - manual table creation when using GORM models
db.DB().Exec("CREATE TABLE users ...")
2. Reset Between Test Cases

Always reset state between test cases to ensure isolation:

// Good ✓
for _, tt := range tests {
    t.Run(tt.name, func(t *testing.T) {
        testutil.T(t).Reset(db)
        tt.fn(t)
    })
}

// Avoid ✗ - tests may interfere with each other
for _, tt := range tests {
    t.Run(tt.name, tt.fn)
}
3. Use Snapshots for Complex State

When you need to return to the same complex state multiple times:

// Good ✓
testutil.T(t).Setup(db)
// ... setup complex test data ...
snapshot := testutil.T(t).Snapshot(db)

// Test case 1
// ... modify data ...
testutil.T(t).Restore(db, snapshot)

// Test case 2 - starts from same state
// ... modify data ...
testutil.T(t).Restore(db, snapshot)
4. Use Fixture Helpers

Prefer fixture helpers over raw SQL when loading test data:

// Good ✓
dbtestutil.MustLoadFixture(t, db.DB(), "users", []map[string]any{
    {"name": "Alice", "email": "alice@example.com"},
    {"name": "Bob", "email": "bob@example.com"},
})

// Acceptable but more verbose ✗
db.DB().Exec("INSERT INTO users (name, email) VALUES (?, ?)", "Alice", "alice@example.com")
db.DB().Exec("INSERT INTO users (name, email) VALUES (?, ?)", "Bob", "bob@example.com")
5. Use Assertions for Validation

Use assertion helpers to make tests more readable:

// Good ✓
dbtestutil.AssertRowCount(t, db.DB(), "users", 5)

// Avoid ✗ - manual count and assertion
var count int64
db.DB().Raw("SELECT COUNT(*) FROM users").Scan(&count)
if count != 5 {
    t.Errorf("count = %d, want 5", count)
}

API Reference

Component
// NewComponent creates a new test database component
func NewComponent() *Component

// WithModels registers models for auto-migration
func (c *Component) WithModels(models ...any) *Component

// DB returns the underlying *gorm.DB
func (c *Component) DB() *gorm.DB

// Component interface methods
Name() string
Start(ctx context.Context) error
Stop(ctx context.Context) error
Health(ctx context.Context) component.Health

// TestComponent interface methods
Reset(ctx context.Context) error
Snapshot(ctx context.Context) (any, error)
Restore(ctx context.Context, snapshot any) error
Fixture Functions
// LoadFixture loads test data into a table
func LoadFixture(db *gorm.DB, table string, data []map[string]any) error

// MustLoadFixture loads test data and fails the test on error
func MustLoadFixture(t *testing.T, db *gorm.DB, table string, data []map[string]any)

// TruncateTable removes all rows from a table
func TruncateTable(db *gorm.DB, table string) error

// TruncateAllTables removes all rows from all tables
func TruncateAllTables(db *gorm.DB) error

// TableExists checks if a table exists
func TableExists(db *gorm.DB, table string) bool

// GetTableNames returns a list of all non-system tables
func GetTableNames(db *gorm.DB) ([]string, error)

// CountRows returns the number of rows in a table
func CountRows(db *gorm.DB, table string) (int64, error)

// AssertTableEmpty fails the test if the table is not empty
func AssertTableEmpty(t *testing.T, db *gorm.DB, table string)

// AssertRowCount fails the test if the table doesn't have the expected row count
func AssertRowCount(t *testing.T, db *gorm.DB, table string, expected int64)

Implementation Notes

  • Uses SQLite in-memory database (:memory:)
  • Thread-safe with mutex-protected operations
  • Automatically handles table schema preservation during Reset
  • Snapshot captures all tables and their data
  • Compatible with GORM models and raw SQL

Examples

See the test files for comprehensive examples:

  • component_test.go - TestComponent lifecycle and state management
  • fixtures_test.go - Fixture helper usage patterns

Documentation

Overview

Package testutil provides testing utilities for the database module.

Package testutil provides testing utilities for the database module.

It includes an in-memory SQLite test component that implements both component.Component and testutil.TestComponent interfaces, along with fixture helpers for loading test data and managing database state.

Quick Start

Create a test database with automatic cleanup:

db := testutil.NewComponent()
testutil.T(t).Setup(db)

// Use db.DB() to access *gorm.DB
db.DB().Exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")

Auto-Migration

Register models for automatic migration on Start():

type User struct {
    ID   uint   `gorm:"primarykey"`
    Name string
}

db := testutil.NewComponent().WithModels(&User{})
testutil.T(t).Setup(db)

State Management

Use Reset, Snapshot, and Restore for test isolation:

// Reset clears all data
testutil.T(t).Reset(db)

// Snapshot captures current state
snapshot := testutil.T(t).Snapshot(db)

// Restore returns to snapshot
testutil.T(t).Restore(db, snapshot)

Fixture Helpers

Load test data easily:

MustLoadFixture(t, db.DB(), "users", []map[string]any{
    {"name": "Alice", "email": "alice@example.com"},
    {"name": "Bob", "email": "bob@example.com"},
})

AssertRowCount(t, db.DB(), "users", 2)

See the README for more examples and best practices.

Migration Driver

MigrationDriver is an in-memory golang-migrate driver fake for exercising migration orchestration (Up/Down/Steps/Reset/Version) without a real database backend. It records the applied version and run count and can be told to fail specific operations, so failure and rollback paths are provable deterministically:

driver := testutil.NewMigrationDriver()
cfg := migration.Config{DB: db, FS: fs, Path: "migrations", Driver: driver.DriverFunc()}
if err := cfg.Up(); err != nil { ... }

// Prove a failing rollback is surfaced, not swallowed:
driver.FailRun()
err := cfg.Down() // wrapped "migrate down" error
Example (BasicUsage)

Example of basic database test component usage

package main

import (
	"context"
	"fmt"

	dbtestutil "github.com/kbukum/gokit/database/testutil"
)

func main() {
	// This would be in a test function
	// t := &testing.T{} // mocked for example

	db := dbtestutil.NewComponent()
	// In real tests: testutil.T(t).Setup(db)
	db.Start(context.TODO())
	defer db.Stop(context.TODO())

	// Create table and insert data
	db.DB().Exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
	db.DB().Exec("INSERT INTO users (name) VALUES (?)", "Alice")

	// Query data
	var name string
	db.DB().Raw("SELECT name FROM users").Scan(&name)

	fmt.Println(name)
}
Output:
Alice
Example (Fixtures)

Example of using fixture helpers

package main

import (
	"context"
	"fmt"

	dbtestutil "github.com/kbukum/gokit/database/testutil"
)

func main() {
	// In real tests, you would pass *testing.T
	db := dbtestutil.NewComponent()
	db.Start(context.TODO())
	defer db.Stop(context.TODO())

	db.DB().Exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)")

	// Load fixture data
	dbtestutil.LoadFixture(db.DB(), "users", []map[string]any{
		{"name": "Alice", "email": "alice@example.com"},
		{"name": "Bob", "email": "bob@example.com"},
	})

	count, _ := dbtestutil.CountRows(db.DB(), "users")
	fmt.Println("User count:", count)

	// Truncate table
	dbtestutil.TruncateTable(db.DB(), "users")

	count, _ = dbtestutil.CountRows(db.DB(), "users")
	fmt.Println("After truncate:", count)

}
Output:
User count: 2
After truncate: 0
Example (Reset)

Example of using Reset for test isolation

package main

import (
	"context"
	"fmt"

	dbtestutil "github.com/kbukum/gokit/database/testutil"
)

func main() {
	db := dbtestutil.NewComponent()
	db.Start(context.TODO())
	defer db.Stop(context.TODO())

	db.DB().Exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
	db.DB().Exec("INSERT INTO users (name) VALUES (?)", "Alice")

	var count int64
	db.DB().Raw("SELECT COUNT(*) FROM users").Scan(&count)
	fmt.Println("Before reset:", count)

	// Reset clears all data
	db.Reset(context.TODO())

	db.DB().Raw("SELECT COUNT(*) FROM users").Scan(&count)
	fmt.Println("After reset:", count)

}
Output:
Before reset: 1
After reset: 0
Example (SnapshotRestore)

Example of using Snapshot and Restore

package main

import (
	"context"
	"fmt"

	dbtestutil "github.com/kbukum/gokit/database/testutil"
)

func main() {
	db := dbtestutil.NewComponent()
	db.Start(context.TODO())
	defer db.Stop(context.TODO())

	db.DB().Exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
	db.DB().Exec("INSERT INTO users (name) VALUES (?)", "Alice")

	// Take snapshot
	snapshot, _ := db.Snapshot(context.TODO())

	// Modify data
	db.DB().Exec("INSERT INTO users (name) VALUES (?)", "Bob")
	db.DB().Exec("INSERT INTO users (name) VALUES (?)", "Charlie")

	var count int64
	db.DB().Raw("SELECT COUNT(*) FROM users").Scan(&count)
	fmt.Println("After modifications:", count)

	// Restore to snapshot
	db.Restore(context.TODO(), snapshot)

	db.DB().Raw("SELECT COUNT(*) FROM users").Scan(&count)
	fmt.Println("After restore:", count)

}
Output:
After modifications: 3
After restore: 1
Example (TestManager)

Example of using TestManager with database component

package main

import (
	"context"
	"fmt"

	dbtestutil "github.com/kbukum/gokit/database/testutil"
	"github.com/kbukum/gokit/testutil"
)

func main() {
	manager := testutil.NewManager(context.TODO())

	db := dbtestutil.NewComponent()
	manager.Add(db)

	// Start all components
	manager.StartAll()
	defer manager.Cleanup()

	// Use the database
	dbComp := manager.Get("database-test").(*dbtestutil.Component)
	dbComp.DB().Exec("CREATE TABLE users (id INTEGER PRIMARY KEY)")

	exists := dbtestutil.TableExists(dbComp.DB(), "users")
	fmt.Println("Table exists:", exists)

}
Output:
Table exists: true
Example (WithModels)

Example of using models with auto-migration

package main

import (
	"context"
	"fmt"

	dbtestutil "github.com/kbukum/gokit/database/testutil"
)

func main() {
	type User struct {
		ID   uint `gorm:"primarykey"`
		Name string
	}

	db := dbtestutil.NewComponent().WithModels(&User{})
	db.Start(context.TODO())
	defer db.Stop(context.TODO())

	// Table is automatically created
	db.DB().Create(&User{Name: "Bob"})

	var user User
	db.DB().First(&user)

	fmt.Println(user.Name)
}
Output:
Bob

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func AssertRowCount

func AssertRowCount(t *testing.T, db *gorm.DB, table string, expected int64)

AssertRowCount fails the test if the table doesn't have the expected row count.

func AssertTableEmpty

func AssertTableEmpty(t *testing.T, db *gorm.DB, table string)

AssertTableEmpty fails the test if the table is not empty.

func CountRows

func CountRows(db *gorm.DB, table string) (int64, error)

CountRows returns the number of rows in a table.

func GetTableNames

func GetTableNames(db *gorm.DB) ([]string, error)

GetTableNames returns a list of all non-system tables.

func LoadFixture

func LoadFixture(db *gorm.DB, table string, data []map[string]any) error

LoadFixture loads test data into a table. Data should be a slice of maps where each map represents a row.

func MustLoadFixture

func MustLoadFixture(t *testing.T, db *gorm.DB, table string, data []map[string]any)

MustLoadFixture loads test data and fails the test on error.

func TableExists

func TableExists(db *gorm.DB, table string) bool

TableExists checks if a table exists in the database.

func TruncateAllTables

func TruncateAllTables(db *gorm.DB) error

TruncateAllTables removes all rows from all tables in the database.

func TruncateTable

func TruncateTable(db *gorm.DB, table string) error

TruncateTable removes all rows from a table.

Types

type Component

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

Component is a test database component that uses SQLite in-memory. It implements both component.Component and testutil.TestComponent interfaces.

func NewComponent

func NewComponent() *Component

NewComponent creates a new test database component. By default, it uses SQLite in-memory database.

func (*Component) DB

func (c *Component) DB() *gorm.DB

DB returns the underlying *gorm.DB, or nil if not started.

func (*Component) Health

func (c *Component) Health(ctx context.Context) component.Health

Health returns the health status of the test database.

func (*Component) Name

func (c *Component) Name() string

Name returns the component name.

func (*Component) Reset

func (c *Component) Reset(ctx context.Context) error

Reset clears all data from all tables while preserving the schema. This is useful for resetting state between test cases.

func (*Component) Restore

func (c *Component) Restore(ctx context.Context, snap any) error

Restore returns the database to a previously captured snapshot state. The snapshot must have been created by the Snapshot method.

func (*Component) Snapshot

func (c *Component) Snapshot(ctx context.Context) (any, error)

Snapshot captures the current state of the database. Returns a snapshot that can be used with Restore to return to this state.

func (*Component) Start

func (c *Component) Start(ctx context.Context) error

Start initializes the in-memory SQLite database.

func (*Component) Stop

func (c *Component) Stop(ctx context.Context) error

Stop closes the database connection.

func (*Component) WithModels

func (c *Component) WithModels(models ...any) *Component

WithModels registers models for auto-migration on Start. This is useful when you want the component to automatically create tables for your models during component startup.

type MigrationDriver

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

MigrationDriver is an in-memory golang-migrate database driver for exercising migration orchestration (Up/Down/Steps/Reset/Version) without a real database backend. It records the applied version and the number of statements run, and can be told to fail specific operations so failure and rollback paths are provable deterministically.

A zero MigrationDriver reports version -1 (no migrations applied); use NewMigrationDriver.

func NewMigrationDriver

func NewMigrationDriver() *MigrationDriver

NewMigrationDriver returns a MigrationDriver with no migrations applied.

func (*MigrationDriver) Close

func (d *MigrationDriver) Close() error

Close implements migratedb.Driver; the fake holds no resources.

func (*MigrationDriver) DriverFunc

func (d *MigrationDriver) DriverFunc() migration.DriverFunc

DriverFunc adapts the fake to a migration.DriverFunc, ignoring the *sql.DB it is handed.

func (*MigrationDriver) Drop

func (d *MigrationDriver) Drop() error

Drop implements migratedb.Driver; it clears the recorded version unless FailDrop is set.

func (*MigrationDriver) FailDrop

func (d *MigrationDriver) FailDrop() *MigrationDriver

FailDrop makes Drop calls fail, simulating a failed schema drop during Reset.

func (*MigrationDriver) FailRun

func (d *MigrationDriver) FailRun() *MigrationDriver

FailRun makes the next and subsequent Run calls fail, simulating a failing migration statement.

func (*MigrationDriver) FailSetVersion

func (d *MigrationDriver) FailSetVersion() *MigrationDriver

FailSetVersion makes SetVersion calls fail, simulating a schema-version write failure.

func (*MigrationDriver) Lock

func (d *MigrationDriver) Lock() error

Lock implements migratedb.Driver.

func (*MigrationDriver) Open

Open implements migratedb.Driver; it returns the receiver unchanged.

func (*MigrationDriver) Run

func (d *MigrationDriver) Run(r io.Reader) error

Run implements migratedb.Driver; it drains the statement and records the run unless FailRun is set.

func (*MigrationDriver) Runs

func (d *MigrationDriver) Runs() int

Runs returns how many migration statements have been applied.

func (*MigrationDriver) SetVersion

func (d *MigrationDriver) SetVersion(version int, dirty bool) error

SetVersion implements migratedb.Driver; it records the version unless FailSetVersion is set.

func (*MigrationDriver) Unlock

func (d *MigrationDriver) Unlock() error

Unlock implements migratedb.Driver.

func (*MigrationDriver) Version

func (d *MigrationDriver) Version() (version int, dirty bool, err error)

Version implements migratedb.Driver; it reports the recorded version and dirty flag.

Jump to

Keyboard shortcuts

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