Skip to content

Factories and Seeders

devituz edited this page Jun 3, 2026 · 1 revision

Factories & seeders

Factories build realistic test data; seeders are named runners that populate the database in a dependency-ordered way. Both come straight out of the box with lago make:factory and lago make:seeder (see CLI-Reference).

Factories

A factory is a generic *factory.Factory[T] constructed from a definition function that returns a fresh T:

import (
    "github.com/devituz/lagodev/database"
    "github.com/devituz/lagodev/factory"
    "myapp/models"
)

func UserFactory(conn *database.Connection) *factory.Factory[models.User] {
    return factory.New(conn, func(f *factory.Faker) models.User {
        return models.User{
            Name:    f.Name(),
            Email:   f.Email(),
            IsAdmin: false,
        }
    })
}

Every call to UserFactory(conn) returns a fresh builder, so you can spin up multiple variants in a single test without state bleed.

Building vs. persisting

u  := UserFactory(conn).MakeOne()              // build, don't insert
us := UserFactory(conn).Count(5).Make()        // []User without IDs

u,  err := UserFactory(conn).CreateOne(ctx)    // INSERT, ID populated
us, err := UserFactory(conn).Count(100).Create(ctx)

Make / MakeOne are pure — useful when you want to assert on a shape without touching the database. Create / CreateOne run the ORM's Save so every persisted row goes through the same hook chain.

States

A state is a function that mutates the model in place. Use it for named variants:

adminState := func(f *factory.Faker, u *models.User) {
    u.IsAdmin = true
}

admins, _ := UserFactory(conn).
    State(adminState).
    Count(3).
    Create(ctx)

States compose — chaining .State(adminState).State(verifiedState) applies both, in order. This is how you model "an admin who is also verified and has 2FA enabled" without exploding the factory's definition into a thousand-variant switch.

One-off overrides

Set is sugar for an inline closure that pins a specific field:

UserFactory(conn).
    Set(func(u *models.User) { u.TenantID = 7 }).
    Count(10).
    Create(ctx)

The closure runs after the base definition and after any states, so Set always wins.

Hooks

UserFactory(conn).
    AfterMake(func(u *models.User) {
        u.Email = strings.ToLower(u.Email)
    }).
    AfterCreate(func(ctx context.Context, u *models.User) error {
        return enqueueWelcomeEmail(ctx, u.ID)
    }).
    Count(50).
    Create(ctx)

AfterMake runs for both Make and Create; AfterCreate runs only after a successful insert. The async AfterCreate receives a context.Context and an error — perfect for queue dispatch or related-row creation.

Deterministic data

For reproducible test fixtures, seed the faker:

fk := factory.NewSeeded(42)
// fk is a *Faker pinned to seed 42 — every faker.Name(), faker.Email(), etc.
// returns the same sequence across runs.

factory.NewSeeded is what lagotest.SQLite uses internally so test output stays stable across CI runs.

Seeders

A seeder is a named runner. Implement the Seeder interface (and optionally Dependent):

import (
    "context"

    "github.com/devituz/lagodev/database"
    "github.com/devituz/lagodev/seeder"
    "myapp/factories"
)

type UserSeeder struct{}

func (UserSeeder) Name() string             { return "UserSeeder" }
func (UserSeeder) Dependencies() []string   { return nil }
func (UserSeeder) Run(ctx context.Context, conn *database.Connection) error {
    _, err := factories.UserFactory(conn).Count(20).Create(ctx)
    return err
}

func init() { seeder.Register(&UserSeeder{}) }

Then either run all seeders:

lago db:seed

Or a single one (its declared dependencies still run first):

lago db:seed UserSeeder
lago db:seed --class UserSeeder           # Laravel-style alias

Dependency ordering

type PostSeeder struct{}

func (PostSeeder) Name() string             { return "PostSeeder" }
func (PostSeeder) Dependencies() []string   { return []string{"UserSeeder"} }
func (PostSeeder) Run(ctx context.Context, conn *database.Connection) error { /* ... */ }

The runner topologically sorts the dependency graph; cycles fail loudly with a clear error pointing at the offending pair. The same seeder is never run twice in a single execution, even if it appears in multiple dependency chains.

Transactional seeders

lago db:seed --transactional

Each seeder runs inside its own transaction; a failure rolls that seeder back without affecting the previously-run ones. The default non-transactional mode is faster for huge fixtures but leaves partial state on failure.

Programmatic

import "github.com/devituz/lagodev/seeder"

runner := seeder.NewRunner(conn, nil, seeder.Options{
    Transactional: true,
    Logger:        appLogger,
    Only:          []string{"UserSeeder", "PostSeeder"},
})
_ = runner.Run(ctx)

Only limits the runner to the named seeders (plus their dependencies). Useful for test setup that needs a specific subset of fixtures.

Faker reference

factory.Faker wraps brianvoe/gofakeit. The pre-baked methods are typed and frequently-used:

Method Returns
Name() / FirstName() / LastName() Random person name
Email() / UserName() Email / username
Phone() Phone number
URL() / UUID() URL / UUID v4
Address() / City() / Country() Geographic strings
Password() Strong 16-char password
Word() / Sentence(n) Short text
Paragraph(p, s, w, sep) Lorem-ipsum-style block
Bool() Random bool
IntRange(lo, hi) Bounded integer
Float64Range(lo, hi) Bounded float
PickString([]string{...}) One of the slice values
Raw() Underlying *gofakeit.Faker for anything else

When --fields includes well-known column names (email, phone, title, body, …), lago make:factory wires the matching faker method automatically:

lago make:factory PostFactory --model=Post \
    --fields="title:string,body:text,published_at:datetime"

# Generates:
#   Title:       f.Sentence(5),
#   Body:        f.Paragraph(2, 3, 5, " "),
#   PublishedAt: time.Now(),

If you ever need something the wrapper doesn't expose, drop to f.Raw() and call the underlying gofakeit method.

See also

Clone this wiki locally