testing

package
v0.9.13 Latest Latest
Warning

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

Go to latest
Published: Feb 12, 2026 License: MIT Imports: 11 Imported by: 0

README

Velocity Database Testing

Model factories and test isolation utilities for Velocity.

Features

  • ✅ Fluent factory API for generating test data
  • ✅ Faker integration for realistic data (gofakeit/v6)
  • ✅ RefreshDatabase for test isolation
  • ✅ State and Sequence support
  • ✅ Cross-database compatibility

Quick Start

1. Setup Test Environment

Create .env.testing in your project root:

APP_ENV=testing

DB_DRIVER=sqlite
DB_DATABASE=:memory:

# Or use a dedicated test database:
# DB_DRIVER=postgres
# DB_DATABASE=myapp_test
# DB_HOST=localhost
# DB_USERNAME=postgres
# DB_PASSWORD=secret

tests/bootstrap_test.go:

package tests

import (
    "os"
    "testing"

    "github.com/joho/godotenv"
    "github.com/velocitykode/velocity/pkg/orm"
    _ "myapp/migrations"
)

func TestMain(m *testing.M) {
    // Load .env.testing
    godotenv.Load("../.env.testing")

    // Initialize from environment
    orm.Init(
        os.Getenv("DB_DRIVER"),
        map[string]any{
            "database": os.Getenv("DB_DATABASE"),
            "host":     os.Getenv("DB_HOST"),
            "username": os.Getenv("DB_USERNAME"),
            "password": os.Getenv("DB_PASSWORD"),
        },
    )
    defer orm.Close()

    m.Run()
}

Now your tests always use the test database automatically! 🎉

2. Define Factories
package tests

import ormtesting "github.com/velocitykode/velocity/pkg/orm/testing"

func UserFactory() *ormtesting.Factory {
    faker := ormtesting.Faker()

    factory := ormtesting.NewFactory("users", func() map[string]interface{} {
        return map[string]interface{}{
            "name":  faker.Name(),
            "email": faker.Email(),
        }
    })

    factory.DefineState("admin", map[string]interface{}{
        "role": "admin",
    })

    return factory
}
2. Write Tests

Option A: LazyRefreshDatabase (Fast - recommended):

func TestExample(t *testing.T) {
    tc := ormtesting.NewTestCase(t)
    tc.LazyRefreshDatabase() // Migrations run once, transaction per test

    // Your test - automatically rolled back after
    user := UserFactory().Create()
    assert.NotNil(t, user)
}

Option B: RefreshDatabase (Thorough - for DDL changes):

func TestExample(t *testing.T) {
    tc := ormtesting.NewTestCase(t)
    tc.RefreshDatabase() // Drops all tables, runs migrations EVERY test

    // Your test
    user := UserFactory().Create()
}

When to use each:

  • LazyRefreshDatabase: 99% of tests (fast, transactions)
  • RefreshDatabase: Tests that modify schema or disable constraints

Factory API

Basic Methods
factory.Make()                   // Generate in-memory (no DB)
factory.Create()                 // Generate and persist to DB
factory.Count(10)                // Generate 10 records
factory.State("admin")           // Apply named state
factory.Sequence("email", fn)    // Sequential values
Chaining
UserFactory().
    Count(50).
    State("verified").
    Sequence("email", func(i int) interface{} {
        return fmt.Sprintf("user%d@test.com", i)
    }).
    Create(map[string]interface{}{
        "created_at": time.Now(),
    })

Test Isolation Methods

Runs migrations once, wraps each test in a transaction:

func TestSomething(t *testing.T) {
    tc := ormtesting.NewTestCase(t)
    tc.LazyRefreshDatabase()

    // Test runs in transaction - rolled back automatically
    UserFactory().Count(100).Create()
}

Performance: ~1ms per test ⚡

RefreshDatabase (Thorough)

Drops all tables and re-runs migrations for each test:

func TestSomething(t *testing.T) {
    tc := ormtesting.NewTestCase(t)
    tc.RefreshDatabase()

    // Completely fresh database
}

Performance: ~15ms per test (SQLite :memory:)

Safety

RefreshDatabase has built-in safety checks:

  • ✅ Requires testing.T (only works in tests)
  • ✅ Checks APP_ENV != "production"
  • ✅ Validates database name contains "test" or is ":memory:"

Faker

Access to gofakeit library:

faker := ormtesting.Faker()

faker.Name()              // "John Doe"
faker.Email()             // "john@example.com"
faker.Phone()             // "+1-555-0123"
faker.City()              // "San Francisco"
faker.Sentence(5)         // Random sentence
faker.Paragraph(3,5,10," ") // Random paragraph
faker.UUID()              // "550e8400-e29b..."
faker.Number(1, 100)      // Random number
faker.Bool()              // true or false

Example Test

func TestPostsIndex(t *testing.T) {
    ormtesting.RefreshDatabase(t)

    // Setup
    user := UserFactory().Create()
    userID := user.(map[string]interface{})["id"]

    PostFactory().Count(3).Create(map[string]interface{}{
        "user_id":   userID,
        "published": true,
    })

    // Test
    router := gin.New()
    router.GET("/posts", controllers.PostsIndex)

    w := httptest.NewRecorder()
    req := httptest.NewRequest("GET", "/posts", nil)
    router.ServeHTTP(w, req)

    // Assert
    assert.Equal(t, 200, w.Code)
    var posts []map[string]interface{}
    json.Unmarshal(w.Body.Bytes(), &posts)
    assert.Equal(t, 3, len(posts))
}

See Also

  • /specs/002-database-testing-system/contracts/factory-api.md - Full factory API
  • /specs/002-database-testing-system/contracts/refresh-api.md - RefreshDatabase API
  • /specs/002-database-testing-system/quickstart.md - Complete tutorial

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func DropAllTables

func DropAllTables(db *sql.DB, driver string, dbName ...string) error

DropAllTables drops all tables in the database

func F

func F() *gofakeit.Faker

F is a convenience alias for Faker()

func Faker

func Faker() *gofakeit.Faker

Faker returns the global faker instance

func GetAllTables

func GetAllTables(db *sql.DB, driver string, dbName ...string) ([]string, error)

GetAllTables returns a list of all tables in the database. For MySQL, the dbName parameter is used for the information_schema query.

func RefreshDatabase

func RefreshDatabase(t *testing.T, manager *orm.Manager) *sql.DB

RefreshDatabase resets the database to a clean state and runs all migrations.

This function: 1. Validates it's safe to run (test database, not production) 2. Drops all existing tables 3. Runs all registered migrations via migrate.Up()

Safety checks: - Requires testing.T (only callable from tests) - Checks APP_ENV != "production" - Verifies database name contains "test" or is ":memory:"

Usage:

func TestExample(t *testing.T) {
    testing.RefreshDatabase(t, manager)
    // Test with clean database
}

func ResetSchemaRefresh added in v0.7.0

func ResetSchemaRefresh()

ResetSchemaRefresh resets the schema refresh flag (for testing the testing framework)

func TruncateAllTables

func TruncateAllTables(db *sql.DB, driver string, dbName ...string) error

TruncateAllTables clears all data from tables (faster than drop/recreate)

Types

type Factory

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

Factory represents a model factory for generating test data

func NewFactory

func NewFactory(manager *orm.Manager, tableName string, definition func() map[string]interface{}) *Factory

NewFactory creates a new factory for generating test data. The manager parameter is required for Create() (database persistence). Pass nil if you only use Make() (in-memory generation).

func (*Factory) Count

func (f *Factory) Count(n int) *Factory

Count sets the number of records to generate

func (*Factory) Create

func (f *Factory) Create(overrides ...map[string]interface{}) interface{}

Create generates data and persists to database

func (*Factory) DefineState

func (f *Factory) DefineState(name string, attributes map[string]interface{})

DefineState defines a named attribute preset

func (*Factory) Make

func (f *Factory) Make(overrides ...map[string]interface{}) interface{}

Make generates data without persisting to database

func (*Factory) Sequence

func (f *Factory) Sequence(field string, generator func(int) interface{}) *Factory

Sequence defines a sequential value generator for a field

func (*Factory) State

func (f *Factory) State(name string) *Factory

State applies a named state to the factory

type ModelFactory added in v0.6.7

type ModelFactory[T any] struct {
	// contains filtered or unexported fields
}

ModelFactory is a type-safe factory for creating test models Usage:

func (User) Factory(m *orm.Manager) *ModelFactory[User] {
    return NewModelFactory[User](m, func() *User {
        return &User{Name: Faker().Name(), Email: Faker().Email()}
    })
}

// In tests:
user := models.User{}.Factory().Create(nil)
admin := models.User{}.Factory().Create(&models.User{Role: "admin"})
users := models.User{}.Factory().Count(3).Create(nil)

func NewModelFactory added in v0.6.7

func NewModelFactory[T any](manager *orm.Manager, definition func() *T) *ModelFactory[T]

NewModelFactory creates a new type-safe model factory

func (*ModelFactory[T]) Count added in v0.6.7

func (f *ModelFactory[T]) Count(n int) *ModelFactory[T]

Count sets the number of records to generate

func (*ModelFactory[T]) Create added in v0.6.7

func (f *ModelFactory[T]) Create(overrides *T) any

Create generates and persists model(s) to database Returns *T for single, []*T for multiple (when Count > 1)

func (*ModelFactory[T]) CreateMany added in v0.6.7

func (f *ModelFactory[T]) CreateMany(count int, overrides *T) []*T

CreateMany is a convenience method that always returns []*T (not any)

func (*ModelFactory[T]) CreateOne added in v0.6.7

func (f *ModelFactory[T]) CreateOne(overrides *T) *T

CreateOne is a convenience method that always returns *T (not any)

func (*ModelFactory[T]) DefineState added in v0.6.7

func (f *ModelFactory[T]) DefineState(name string, modifier func(*T)) *ModelFactory[T]

DefineState registers a named state modifier

func (*ModelFactory[T]) Make added in v0.6.7

func (f *ModelFactory[T]) Make(overrides *T) any

Make generates model(s) without persisting to database Returns *T for single, []*T for multiple (when Count > 1)

func (*ModelFactory[T]) State added in v0.6.7

func (f *ModelFactory[T]) State(name string) *ModelFactory[T]

State applies a named state modifier to the factory

type TestCase

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

TestCase provides test helpers for database testing

func NewTestCase

func NewTestCase(t *testing.T, manager *orm.Manager) *TestCase

NewTestCase creates a new test case instance. The manager must be an initialized *orm.Manager with an active connection.

func Setup

func Setup(t *testing.T, manager *orm.Manager) *TestCase

Setup creates a TestCase and automatically runs RefreshDatabase This is the recommended way to setup database tests

Usage:

func TestExample(t *testing.T) {
    tc := ormtesting.Setup(t, manager)
    // Database already refreshed - ready to test
}

If you don't need database refresh, use NewTestCase(t, manager) directly instead

func (*TestCase) DB

func (tc *TestCase) DB() *sql.DB

DB returns the database connection

func (*TestCase) LazyRefreshDatabase

func (tc *TestCase) LazyRefreshDatabase()

LazyRefreshDatabase resets the database for testing: 1. Runs migrations ONCE per test suite (not per test) 2. Truncates all tables before each test (fast cleanup)

Usage:

func TestExample(t *testing.T) {
    tc := testing.NewTestCase(t, manager)
    tc.LazyRefreshDatabase()

    // Test code - clean database, fast setup
}

func (*TestCase) RefreshDatabase

func (tc *TestCase) RefreshDatabase()

RefreshDatabase drops all tables and runs migrations for EACH test This is slower but more thorough - use when you need true isolation (e.g., testing migrations themselves)

Usage:

func TestExample(t *testing.T) {
    tc := testing.NewTestCase(t, manager)
    tc.RefreshDatabase()

    // Test code - completely fresh database
}

Jump to

Keyboard shortcuts

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