standin is a command-line tool that generates plain test fixture functions from Go structs.
Unlike factory libraries that bring a runtime API or a builder DSL into your tests, standin does all its work at generate time and emits the same code you would write by hand — a plain function per struct, prefilled with fake data via gofakeit.
The core idea is simple:
- Your model structs declare what a fixture looks like.
- standin infers how to fill each field with fake data.
- The output is plain Go you could have written yourself.
Because fixtures are regenerated from the models, they never silently drift: add a field to a struct, run go generate,
and every fixture picks it up.
go install github.com/mickamy/standin@latestOr manage it as a module tool (Go 1.24+):
go get -tool github.com/mickamy/standinGiven a model package:
package model
import "time"
type User struct {
ID int64
Name string
Email string
Age int `fake:"{number:18,65}"`
Bio *string
CreatedAt time.Time
}Add a directive and run go generate ./...:
//go:generate standin -source ./model -destination ./fixture(Use go tool standin instead when you manage it as a module tool.)
Result (fixture/fixture_gen.go):
// Code generated by standin. DO NOT EDIT.
package fixture
import (
"github.com/brianvoe/gofakeit/v7"
"github.com/example/myapp/model"
)
func User(setters ...func(m *model.User)) model.User {
m := model.User{
ID: gofakeit.Int64(),
Name: gofakeit.Name(),
Email: gofakeit.Email(),
Age: gofakeit.Number(18, 65),
CreatedAt: gofakeit.Date(),
}
for _, s := range setters {
s(&m)
}
return m
}Use it in tests:
u := fixture.User() // everything fake
alice := fixture.User(func (m *model.User) {
m.Name = "Alice" // override only what the test cares about
})The generated code depends only on gofakeit, plus the packages of the well-known types it fills (currently
github.com/google/uuid); standin reminds you to run go mod tidy when the destination module is missing one.
standin -source <pkg> -destination <dir> [flags]
| Flag | Description |
|---|---|
-source <pkg> |
Source package to scan (relative path or import path). Required. |
-destination <dir> |
Output directory for fixture_gen.go. Required. |
-package <name> |
Generated package name. Default: the package the destination directory already declares, else its directory name. |
-exclude <names> |
Comma-separated type names to exclude (e.g., -exclude Foo,Bar). |
--version, -v |
Print version. |
--help, -h |
Show help. |
An -exclude name that matches no struct produces a warning, and excluding every struct is an error — the existing
generated file is left untouched.
Every exported struct in the source package gets a fixture, so adding a model never requires touching the directive. To
opt a struct out, use -exclude or a doc-comment directive:
// Draft is assembled manually in tests.
//
//standin:ignore
type Draft struct {
// ...
}The directive follows the Go directive convention: it must be written exactly as //standin:ignore, with no space after
the comment marker. A doc comment that merely mentions standin:ignore in prose does not exclude the type.
For each exported field, standin picks a value in the following priority. Anything it cannot infer confidently is left as the zero value — predictability over coverage.
fakestruct tag — gofakeit's existing tag convention, so models already tagged forgofakeit.Structwork as-is.- Field name — a small table of high-confidence names, applied only when the type matches.
- Field type — basic types and a small table of well-known named types (
time.Time,uuid.UUID). - Structs from the source package — a value field whose type has its own fixture calls it:
Profile: Profile(). - Everything else — zero value.
| Tag | Result |
|---|---|
fake:"skip" / fake:"-" |
zero value |
fake:"" |
ignored — normal inference applies |
fake:"{email}" and other known templates |
direct call (tables below) |
any other template on a string field |
mustGenerate("...") |
| any other template on a non-string field | zero value |
mustGenerate is a small helper emitted into the generated file; it wraps the two-value gofakeit.Generate and panics
on invalid templates, so a broken template fails loudly at fixture construction time.
A known template whose field type does not match falls through the same way: fake:"{date}" on a string field becomes
mustGenerate("{date}"), while fake:"{email}" on an int field stays zero.
Known no-parameter templates, applied when the field type matches:
| Template | Call | Field type |
|---|---|---|
{email} {firstname} {lastname} {name} {phone} {url} {word} {city} {country} |
the matching gofakeit call | string |
{uuid} |
gofakeit.UUID() |
string |
{date} |
gofakeit.Date() |
time.Time |
{uuid} |
uuid.MustParse(gofakeit.UUID()) |
uuid.UUID |
Known parameterized templates, with arguments validated as Go literals:
| Template | Call | Result type |
|---|---|---|
{number:1,10} |
gofakeit.Number(1, 10) |
int |
{intrange:-5,5} |
gofakeit.IntRange(-5, 5) |
int |
{uintrange:1,10} |
gofakeit.UintRange(1, 10) |
uint |
{float32range:0.5,2.5} |
gofakeit.Float32Range(0.5, 2.5) |
float32 |
{float64range:0.5,2.5} |
gofakeit.Float64Range(0.5, 2.5) |
float64 |
{price:1.00,10.00} |
gofakeit.Price(1.00, 10.00) |
float64 |
{sentence:10} |
gofakeit.Sentence(10) |
string |
When a numeric result differs from the field's kind, standin wraps the call in a conversion: ID int64 with
fake:"{number:1,10}" becomes int64(gofakeit.Number(1, 10)).
Arguments must be representable both by the call's parameter type (int and uint arguments are checked at 32 bits so
the emitted literals compile on 32-bit platforms) and by the field's type. A tag that fails either check falls back to
the zero value instead of wrapping out-of-range values: fake:"{intrange:-5,5}" on a uint8 field is rejected, not
truncated.
Tags also work on named basic types declared in the source package: Status Status with fake:"{word}" becomes
model.Status(gofakeit.Word()). Named types from other packages stay zero values.
Applied only when the field type matches the call's result type.
| Field name | Call | Field type |
|---|---|---|
Email |
gofakeit.Email() |
string |
Name |
gofakeit.Name() |
string |
FirstName |
gofakeit.FirstName() |
string |
LastName |
gofakeit.LastName() |
string |
Phone |
gofakeit.Phone() |
string |
URL |
gofakeit.URL() |
string |
UUID |
gofakeit.UUID() |
string |
Address |
gofakeit.Address().Address |
string |
City |
gofakeit.City() |
string |
Country |
gofakeit.Country() |
string |
*At (suffix) |
gofakeit.Date() |
time.Time |
| Field type | Call |
|---|---|
string |
gofakeit.Word() |
bool |
gofakeit.Bool() |
int int8 int16 int32 int64 |
the matching gofakeit.Int*() |
uint uint8 uint16 uint32 uint64 |
the matching gofakeit.Uint*() |
float32 float64 |
gofakeit.Float32() / gofakeit.Float64() |
time.Time |
gofakeit.Date() |
uuid.UUID (github.com/google/uuid) |
uuid.MustParse(gofakeit.UUID()) |
Type aliases are resolved to their underlying type before inference.
The named types in this table are the only ones standin fills from other packages, and every value still comes from
gofakeit so a single gofakeit.Seed keeps fixtures reproducible. When a fixture uses one, the generated file imports
that package alongside gofakeit.
The following stay at their zero value, on purpose:
- Pointers —
nilrespects nullable semantics; set one with a setter when a test needs it. - Slices, maps, interfaces, channels, funcs —
nil. - Named basic types (e.g.,
type Status string) — standin cannot know the valid values; add afaketag to opt in (see the tag section above). - Named types from other packages other than the well-known ones listed above.
- Unexported fields — not settable from the fixture package.
Foreign keys and associations are intentionally out of scope: fixtures fill values, and the setter is the escape hatch for wiring IDs together.
These are two separate things:
The tool's output is deterministic. The same input produces byte-identical output: types are emitted in name order, fields in definition order, and the result is gofmt-formatted. This makes a CI drift check reliable:
go generate ./... && git diff --exit-codeThe runtime values are random. Generated code calls gofakeit's global faker. For reproducible tests, seed it once:
func TestMain(m *testing.M) {
_ = gofakeit.Seed(1)
os.Exit(m.Run())
}- Generic structs are skipped with a warning.
- The source and destination must be different packages.
- One file (
fixture_gen.go) per destination; per-type file splitting may come later.
See example/ for a complete module: models, the go:generate directive, the committed generated
fixtures, and tests that use them. Regenerate and run it with:
make test-exampleMost Go factory libraries decide values at runtime through reflection or a builder API. standin explores the same point in the design space as sqlc and mockery: resolve everything at generate time and emit plain code.
| Tool | Values decided | Test-side API | Output |
|---|---|---|---|
| fixtory | generate time | builder DSL (NewBuilder().EachParam().BuildList()) |
typed builders |
| factory-go | runtime | factory objects + reflection | — |
gofakeit.Struct |
runtime | reflection over tags | — |
| standin | generate time | none — plain function calls | plain functions |
standin works best when:
- fixtures should read like hand-written code,
- models change often enough that drift is a real risk,
- tests should not depend on a factory framework.