Skip to content

Introduction

Fabricator builds typed test data for Go structs. You describe the fields a test actually cares about; everything else is generated.

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 as well. The one 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, valid value, and 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 this test.

Field configuration goes through a typed reference:

renewsAt := fabricator.FieldOf[Subscription, time.Time]("RenewsAt")

FieldOf[T, V] verifies, at the moment you construct it, that T has a field called RenewsAt, that it is exported, and that it accepts a time.Time. A typo or a changed field type fails right there with a clear message, instead of silently doing nothing until an assertion fails somewhere else.

UnsafeFieldOf skips those checks and defers them to build time. It is the escape hatch for the rare case where the field name is not known statically.

Unconfigured fields are filled by go-faker. That is convenient, and it is also the overwhelming majority of what a build costs — faker walks T reflectively and generates a value for every field.

When a test wants exact fixtures rather than plausible ones, WithoutFaker starts from the zero value instead. It is both clearer and dramatically cheaper. On a struct of four scalar fields:

B/op allocs/op
Build 2,128 45
Build with WithoutFaker 64 1
go test -bench . on an Apple M4 Pro, Go 1.27. Allocation counts are quoted rather than ns/op because they are deterministic and the wall-clock figures are machine-dependent; on this shape the time difference is roughly 50x.

The gap widens sharply with collections. Faker’s default maximum slice and map size is 100, so a struct carrying one []Pet and one map[string]string spends around 1,800 allocations per build generating roughly fifty pets and fifty map entries — almost all of which a given test never reads. Either skip generation, or cap the sizes:

fabricator.WithFakerOptions[Person](options.WithRandomMapAndSliceMaxSize(3))

Fabricator is for non-pointer struct types. It does not support nested dotted field paths such as "Address.City" — use a hook to reach into a nested value.

If a test needs one struct with three fields set, a plain struct literal is still the clearest thing to write. Factories earn their keep when the same type is built many times, in many shapes, across many tests.