README
¶
standin
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.
Install
go install github.com/mickamy/standin@latest
Or manage it as a module tool (Go 1.24+):
go get -tool github.com/mickamy/standin
Quickstart
Given 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; standin reminds you to run go mod tidy after the first generation.
CLI reference
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 destination 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.
Inference rules
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
time.Time. - Structs from the source package — a value field whose type has its own fixture calls it:
Profile: Profile(). - Everything else — zero value.
1. Tags
| 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} {uuid} {word} {city} {country} |
the matching gofakeit call | string |
{date} |
gofakeit.Date() |
time.Time |
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.
2. Field names
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 |
3. Field types
| 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() |
Type aliases are resolved to their underlying type before inference.
4. Zero values
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). - Structs from other packages (except
time.Time). - 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.
Determinism and reproducibility
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-code
The 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())
}
Limitations
- 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.
Example
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-example
How standin compares to other fixture approaches
Most 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.
License
Documentation
¶
There is no documentation for this package.