Introduction
Fabricator builds typed test data for Go structs. You describe the fields a test actually cares about; everything else is generated.
The problem
Section titled “The problem”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.
Typed, not stringly typed
Section titled “Typed, not stringly typed”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.
What generation costs
Section titled “What generation costs”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))When not to use it
Section titled “When not to use it”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.