standin

command module
v0.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 28, 2026 License: MIT Imports: 2 Imported by: 0

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, 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.


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 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.


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.

  1. fake struct tag — gofakeit's existing tag convention, so models already tagged for gofakeit.Struct work as-is.
  2. Field name — a small table of high-confidence names, applied only when the type matches.
  3. Field type — basic types and a small table of well-known named types (time.Time, uuid.UUID).
  4. Structs from the source package — a value field whose type has its own fixture calls it: Profile: Profile().
  5. 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} {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.

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()
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.

4. Zero values

The following stay at their zero value, on purpose:

  • Pointersnil respects nullable semantics; set one with a setter when a test needs it.
  • Slices, maps, interfaces, channels, funcsnil.
  • Named basic types (e.g., type Status string) — standin cannot know the valid values; add a fake tag 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.


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

MIT

Documentation

The Go Gopher

There is no documentation for this package.

Directories

Path Synopsis
internal
cli
gen

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL