-
Notifications
You must be signed in to change notification settings - Fork 0
Factories and 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).
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.
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.
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.
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.
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.
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.
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:seedOr a single one (its declared dependencies still run first):
lago db:seed UserSeeder
lago db:seed --class UserSeeder # Laravel-style aliastype 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.
lago db:seed --transactionalEach 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.
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.
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.
-
ORM — the
Savepath your factories drive. -
CLI-Reference —
lago make:factory,lago make:seeder,lago db:seed. - Migrations-and-Schema — the tables your seeders populate.
lagodev on GitHub · MIT-licensed — see LICENSE.