README.md
September 2, 2026 · View on GitHub
Typed test data factories for Go.
Say what matters to the test; let the rest be generated.
Docs · Quickstart · Guides · API
Test data written by hand says too much. A test about expired subscriptions needs one field to be a date in the past, but the struct literal makes you spell out a name, an email, an ID, and a billing address too. The field that matters is buried, and every new field on the struct means editing every literal that constructs it.
Fabricator inverts that. A factory generates a complete value; the test overrides only what it is about.
expired := factory.Build(fabricator.Override(renewsAt, time.Now().Add(-time.Hour)))
Adding a field to Subscription does not touch that test.
Installation
go get github.com/Goldziher/fabricator/v2
Requires Go 1.26 or newer.
Quickstart
type User struct {
ID int
Name string
Email string
Admin bool
}
var (
userID = fabricator.FieldOf[User, int]("ID")
userName = fabricator.FieldOf[User, string]("Name")
userAdmin = fabricator.FieldOf[User, bool]("Admin")
)
factory := fabricator.New(User{},
fabricator.Field(userID, func(ctx fabricator.BuildContext) int {
return ctx.Iteration + 1
}),
)
user := factory.Build() // ID 1, rest generated
admin := factory.Build(fabricator.Override(userAdmin, true))
users := factory.Batch(10) // IDs 1..10
FieldOf[T, V] checks when you construct it that T has that field, that it
is exported, and that it accepts a V. A typo fails there, with a clear message,
instead of silently doing nothing until an assertion fails somewhere else.
What you get
Typed field references. No map[string]any, no stringly-typed configuration
that compiles and then does nothing. UnsafeFieldOf is the explicit escape hatch
when a name genuinely is not known statically.
Generated defaults. Unconfigured fields are filled by
go-faker, including via faker:"..." struct
tags on your model.
Sequences.
fabricator.Field(role, fabricator.Sequence("admin", "editor", "viewer"))
factory.Batch(5) // admin, editor, viewer, admin, editor
Factories derived from factories. Extend builds a variant without restating
the base:
base := fabricator.New(User{}, fabricator.Value(name, "Moishe"), fabricator.Value(role, "user"))
admin := fabricator.Extend(base, fabricator.Value(role, "admin"))
Subfactories for nested structs, pointers, and slices, with *With variants
whose children depend on the parent's build context:
fabricator.Field(favoritePet, fabricator.Subfactory(petFactory))
fabricator.Field(profile, fabricator.PtrSubfactory(profileFactory))
fabricator.Field(pets, fabricator.SliceSubfactory(petFactory, 2))
Lifecycle hooks at three points — AfterFaker, AfterBuild, AfterCreate:
fabricator.AfterBuild(func(user *User, _ fabricator.BuildContext) error {
user.Email = strings.ToLower(user.Name) + "@example.com"
return nil
})
Persistence. Give a factory a handler and build-and-save is one call:
type PersistenceHandler[T any] interface {
Save(ctx context.Context, instance T) (T, error)
SaveMany(ctx context.Context, instances []T) ([]T, error)
}
user := factory.Create(ctx)
users := factory.CreateBatch(ctx, 10)
Errors, not only panics. Build, Batch, Create, and CreateBatch panic
so test bodies stay terse. Each has an E twin — BuildE, BatchE, CreateE,
CreateBatchE — that returns an error instead.
Reproducing a failure
Generated data means a test can fail on a value you cannot see. Seed pins
generation for the process:
func TestMain(m *testing.M) {
fabricator.Seed(42)
os.Exit(m.Run())
}
It seeds both of faker's sources, including the separate one behind
faker:"uuid_*" fields, which would otherwise keep varying. Three limits are
worth knowing, and are spelled out in the
determinism guide:
do not call Seed once builds are running, concurrency still reorders draws, and
-run/-shuffle change where a test lands in the stream.
Exact fixtures, and speed
When a test asserts field by field, generated values in the fields it does not
set are noise. WithoutFaker starts from the zero value:
factory := fabricator.New(User{},
fabricator.WithoutFaker[User](),
fabricator.Value(userName, "Moishe"),
)
factory.Build() // User{Name: "Moishe"} — everything else zero
It is deterministic regardless of seed or ordering, and faker's reflective walk
over T is what a build actually costs. On a struct of four scalar fields:
| B/op | allocs/op | |
|---|---|---|
Build | 2,128 | 45 |
Build with WithoutFaker | 64 | 1 |
go test -bench ., Apple M4 Pro, Go 1.27. Allocation counts are quoted
rather than ns/op because they are deterministic; the wall-clock gap on this
shape is roughly 50x, but the exact figure depends on the machine. It gets far
more extreme with collections: faker's default maximum slice and map size is
100, so a struct with one []Pet and one map[string]string costs about 1,800
allocations per build, essentially all of it generating elements a test will
never look at.
Guides
Full documentation is at goldziher.github.io/fabricator.
- Introduction — the problem and the shape of the solution
- Quickstart
- Fields and values — references, providers, sequences, precedence
- Factories from factories —
Extendand subfactories - Lifecycle hooks
- Persistence
- Determinism
- Errors and panics — every message and its cause
Limits
Factories are for non-pointer struct types. Nested dotted field paths such as
"Author.Name" are not supported — use an AfterBuild hook to reach into a
nested value.
Development
task setup # tooling and git hooks
task test # go test ./...
task test:race # go test -race ./...
task check # vet, golangci-lint, govulncheck, ai-rulez
task lint # poly lint and format check
task update # dependencies, git hooks, and GitHub Actions
Brand assets are generated, not hand-committed:
python3 scripts/generate_assets.py
See CONTRIBUTING.md.