testing

package
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Dec 28, 2025 License: MIT Imports: 9 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) 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) ([]string, error)

GetAllTables returns a list of all tables in the database

func RefreshDatabase

func RefreshDatabase(t *testing.T) *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() 4. Registers cleanup to close the connection after test

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)
    // Test with clean database
}

func TruncateAllTables

func TruncateAllTables(db *sql.DB, driver 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(tableName string, definition func() map[string]interface{}) *Factory

NewFactory creates a new factory for generating test data

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 TestCase

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

TestCase provides test helpers

func NewTestCase

func NewTestCase(t *testing.T) *TestCase

NewTestCase creates a new test case instance

func Setup

func Setup(t *testing.T) *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)
    // Database already refreshed - ready to test
}

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

func (*TestCase) DB

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

DB returns the database connection

func (*TestCase) LazyRefreshDatabase

func (tc *TestCase) LazyRefreshDatabase()

LazyRefreshDatabase drops tables and runs migrations before each test

Usage:

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

    // Test code - fresh database
}

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

Usage:

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

    // Test code - starts with completely fresh database
}

Jump to

Keyboard shortcuts

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