README.md

September 2, 2026 · View on GitHub

Fabricator — typed test data for Go

Typed test data factories for Go.

Say what matters to the test; let the rest be generated.

CI Go Reference Docs Latest Release Go Version License: MIT

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/opallocs/op
Build2,12845
Build with WithoutFaker641

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.

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.

License

MIT