fixture

module
v0.0.22 Latest Latest
Warning

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

Go to latest
Published: Nov 12, 2026 License: MIT

README

fixture

CI codecov Go Reference Release 日本語

kanna-fixture writes a constructor for every struct of a package, with every field already filled, so a test states only what it cares about. Fields are numbered (slug-1, email-1@example.com), so no two fixtures collide, and filled to pass the rules their validate tags declare; a field tagged fake takes a gofakeit template instead.

Part of the kanna family of code generators. Requires Go 1.27.

Install

go get -tool github.com/go-kanna/fixture/cmd/kanna-fixture

The line also requires gofakeit, which the generated code calls for the fields that ask for it and the generator uses to check their templates. Fixtures for a package whose fields ask for nothing do not import it.

Use

package model

//go:generate go tool kanna-fixture

type Role string

const (
	RoleAdmin  Role = "admin"
	RoleMember Role = "member"
)

type User struct {
	ID        int64
	Name      string `fake:"{firstname} {lastname}"`
	Email     string `validate:"required,email"`
	Age       int    `validate:"gte=18,lte=130"`
	Role      Role
	Nickname  *string
	CreatedAt time.Time
	DeletedAt time.Time `fake:"skip"`
}

type Article struct {
	ID     uuid.UUID
	Slug   string
	Title  string
	Author User
}

go generate ./... writes fixture/fixture_gen.go, a package of its own beside the model:

// Code generated by kanna-fixture. DO NOT EDIT.

package fixture

var seqArticle atomic.Int64

// Article returns a new model.Article with every field it can fill set, then
// applies setters in order.
func Article(setters ...func(*model.Article)) model.Article {
	n := seqArticle.Add(1)
	x := model.Article{
		ID:     uuid.MustParse(fmt.Sprintf("00000000-0000-4000-8000-%012x", n)),
		Slug:   fmt.Sprintf("slug-%d", n),
		Title:  fmt.Sprintf("title-%d", n),
		Author: User(),
	}
	for _, set := range setters {
		set(&x)
	}
	return x
}

var seqUser atomic.Int64

// User returns a new model.User with every field it can fill set, then
// applies setters in order.
func User(setters ...func(*model.User)) model.User {
	n := seqUser.Add(1)
	f := gofakeit.NewFaker(rand.NewPCG(uint64(n), 0x5f2c8e0a9d4b7136), false)
	x := model.User{
		ID:        n,
		Name:      mustGenerate(f, "{firstname} {lastname}"),
		Email:     fmt.Sprintf("email-%d@example.com", n),
		Age:       int(18 + (n-1)%113),
		Role:      model.RoleAdmin,
		CreatedAt: time.Date(2000, time.January, 1, 0, 0, int(n), 0, time.UTC),
	}
	for _, set := range setters {
		set(&x)
	}
	return x
}

A test asks for what it needs and changes what it cares about:

a := fixture.Article()
b := fixture.Article(func(a *model.Article) { a.Title = "Generating fixtures" })
// a.Slug == "slug-1" and b.Slug == "slug-2": two articles never collide.
// a.Author.Email == "email-1@example.com", b.Author.Email == "email-2@example.com".

Setters are plain functions, so a team can name the ones it reuses:

member := func(u *model.User) { u.Role = model.RoleMember }
u := fixture.User(member, func(u *model.User) { u.Age = 70 })

The fixtures import the model, so tests in other packages and the model's external tests (package model_test) can use them; the model's internal tests cannot, as that would be an import cycle.

With two or more kanna generators, run them with go tool kanna gen, which runs them in the order they need.

Values

Every call takes the next number n of its type, starting at 1, and each field takes the first rule that matches:

Field Value
tagged fake see The fake tag
a struct that gets a fixture in this run that fixture: Author: User(). One that would recur stays zero, or new(T) for a required pointer, and is warned about
a type with a fixture written by hand that fixture: Status()
an enum, a named string type with constants declared beside it the first constant that is not the zero value: model.RoleAdmin
a value type of kanna-value its constructor's, of what a field named after it would hold
time.Time n seconds past 2000-01-01T00:00:00Z
uuid.UUID, from the standard library or github.com/google/uuid a UUID ending in n: 00000000-0000-4000-8000-000000000001
any other named [16]byte, such as type OrderID uuid.UUID that UUID's bytes: model.OrderID{0, …, 0x40, …, 0x80, …, byte(n)}
any other named type of a basic kind the value of its kind, as below, converted: model.UserID(1)
an integer or a float n, wrapping around in a type too narrow for it
a string named …Email, …URL, or …UUID that format: email-1@example.com, https://example.com/url-1, a UUID
a string the field's name and n: slug-1, first-name-1
a bool false
a pointer, slice, map, interface, channel, func, array, or other struct the zero value

Then the rules of its validate tag adjust the value. Unexported fields are left alone, since the fixtures cannot set them from their own package, and so are fields of a type they cannot name from there: an unexported type, or a struct type written out with an unexported field. Where the zero value they keep breaks the rules, generation warns at the field, as for a value the fixture cannot fill, and names the fixture of the struct to write by hand.

What the number feeds is unique per type, and names its field: title-3 in a failure is the Title of the third Article made. What it does not feed (constants, the first of a oneof, bools) is the same whichever tests ran first, so a test behaves the same alone, in the whole run, and in parallel. The values built from n do move with the number of fixtures made before; a test that asserts on one sets it.

The numbers count within one process. Test binaries that share a database each make their own email-1@example.com, so give each package a schema or a transaction of its own, or set the unique fields.

kanna-fixture does not read the orm tags of kanna-orm, so a model of it is filled like any other: an ID marked orm:",default" gets its number, though Insert leaves it for the database to assign, and so does a foreign key, though no row it refers to was inserted. Where the database is to assign the ID, set it to zero with a setter, and set a foreign key to the ID of the row it refers to.

A pointer stays nil and a slice stays nil, because absent is a value worth keeping: a fixture with a Nickname would erase the difference between a user who has one and a user who does not. required fills them.

Rules

A field's validate tag is read so that its value passes it. The tag is the one go-playground/validator and kanna-validator both read; kanna-fixture depends on neither, and fills the fields the same whichever of them checks the struct, if any does. A fixture is valid as made, so a test of one rule changes one field:

type SignUp struct {
	Email    string `json:"email"    validate:"required,email"`
	Password string `json:"password" validate:"required,min=12"`
	Confirm  string `json:"confirm"  validate:"eqfield=Password"`
	Role     string `json:"role"     validate:"required,oneof=admin member guest"`
	Age      int    `json:"age"      validate:"gte=18,lte=130"`
}
req := fixture.SignUp(func(r *model.SignUp) { r.Email = "nope" })
// Validating req reports Email, and nothing else.
Rule Value
required not the zero value: true, a number other than 0, a pointer to a value, a slice of one element, an empty map
min, max, len on strings and slices a length within them: password-001 for min=12, k elements for min=k, []T{} for required,max=0
gt, gte, lt, lte, min, max on integers n, counted from the lower bound, or from 1, and wrapping within the upper: gt=0 → 1, 2, 3, …
gt, gte, lt, lte, min, max on floats the lowest value they allow, or the highest without a lower bound: gt=0 → 1
eq that value
ne another value: the next choice of a oneof, an integer counted past it, skipping(1+(n-1)%9, 2) for gte=1,lte=10,ne=2, or the lowest float left
oneof=a b c the first: "admin"
eqfield=Field the other field's value
email, url, uuid that format, numbered within min and max: em-0001@example.com for len=19
pattern=regexp, which only kanna-validator has a string gofakeit generates from the expression
rule=Type, which only kanna-validator has the first constant of an enum, or what a field of a value type holds, read through its accessor
dive the rules after it, for each element made

A value the rules fix, by eq, oneof, or bounds that leave one integer, is written as the first exported constant of the field's type that holds it, where one does: model.StatusClosed for oneof=closed open.

A time.Duration is bounded with durations, as kanna-validator reads them: gte=1s,lt=1h counts up from one second, and eq=1s gives time.Second. A number without a unit is read as nanoseconds, as go-playground reads it. Both validators take a type declared on time.Duration as an integer, so its rules count nanoseconds.

Type of rule=Type is read as kanna-validator reads it: a bare name is a type of the struct's package, and pkg.Type one of the package the struct's file imports as pkg, as _ if nothing else uses it.

A field eqfield holds equal to another is copied from it before the setters run, so a setter that changes the other field has to change this one too.

The rules are read to be satisfied, not checked. A rule kanna-fixture does not know, such as go-playground's alphanum, may be broken by the value it fills, so generation warns at the field and asks for a fake tag, and asks too whether a rule it knows was meant where the name is close to one, as requird is; of alternatives such as uuid4|uuid, the one it knows is filled. omitempty, omitnil, and omitzero need nothing, as they only let more values pass. A map is filled empty, so a length it is to have is warned about the same way. A pattern is the one rule a number cannot meet, and gofakeit cannot generate every expression; when what it makes does not match, or does not fit within min, max, or len, generation warns and asks for a fake tag. So it does for a format a rule asks for, email, url, or uuid, that cannot fit within them: a UUID is 36 characters long, and an address or a URL needs room for its number beside @example.com or https://example.com/. A format the name of the field alone asks for gives way to them, and the field is numbered without it.

A rule whose value kanna-fixture cannot read, such as max=abc, or gte=1sec on a time.Duration, is warned about the same way, and so are rules no value of the field's type passes: gte=200 on an int8, gt=0,lt=1, or gte=1h,lte=1m on a time.Duration.

A value the fixture cannot fill stays zero: a struct of another package or one without a fixture, an array, an interface, a channel, or a func. Where that breaks the rules, the field's own or those of the struct's fields, generation warns at the field and names the fixture to write by hand:

./model.go:12:2: warning: kanna-fixture cannot fill rules.Inner, and the zero value it leaves breaks the rules (field In)
  write the fixture of Inner by hand, func Inner() rules.Inner in package fixture, which the fixtures then call
  tag the field fake:"skip" to leave it zero without a warning, and set it where it matters

The fake tag

The fake tag takes gofakeit's own syntax, the tag gofakeit.Struct reads, for a field that wants a realistic value or a format the rules do not describe:

Tag Value
fake:"{firstname} {lastname}" the template, generated: Neha Wang
fake:"{number:18,65}" a number in the range, for a numeric field it fits
fake:"???-####" ? a letter, # a digit: zSW-1523
fake:"123-4567" that value, fixed
fake:"skip" or fake:"-" the zero value

A string field takes any template. A numeric field takes the numeric ones ({number}, {intrange}, {uintrange}, {float32range}, {float64range}, and {price}) with a range that fits it; a time.Time takes {date}, and a UUID, or any other named [16]byte, {uuid}. {date} makes a time down to the nanosecond, in years from 1900 on, which a database may round or not hold, so one read back from it is not equal; leave such a field untagged, for whole seconds past 2000, or write its fixture by hand. A number or a bool also takes a value as it stands, such as fake:"42", and a named type converts what its kind takes. A pointer is set to point to the value, though it would otherwise stay nil. The rules of the field's validate tag are not applied: the template is the value asked for. Where what it makes is known before it runs, a range of numbers or a value as it stands, generation warns when the rules reject it, always or for some of the numbers. fake:"skip" leaves the field zero whatever its rules say, as the way to leave a field the fixture cannot fill.

A tagged field is made by a faker seeded with the fixture's number and its type, so the third User made has the same name on every run, whatever the fixtures of other types do. The same, that is, for one version of gofakeit: another may make other values from a template, so a test that needs a value of its own sets it with a setter. Its values are not unique: leave a unique column untagged.

gofakeit returns a template it does not understand as the value itself, so kanna-fixture checks every template while generating, against the gofakeit the module requires:

$ go generate ./...
./model.go:9:2: error: unknown function "numbr" in fake tag "{numbr:1,10}" (field Age)
  did you mean "number"?
./model.go:10:2: error: fake tag "{email}" cannot fill int (field Count)
./model.go:11:2: error: fake tag "{number:1,300}": 300 does not fit int8 (field Level)
./model.go:14:2: warning: values generated from the pattern do not match it; give the field a fake tag (field Code)
./model.go:15:2: warning: fake tag "{number:1,10}" makes no value that validate:"gte=18" lets through (field Adult)

Structs

Every exported struct in the package gets a fixture. There is nothing to opt into, so a struct added to the model has one by the next go generate.

Directive Effect
//kanna:fixture skip do not generate a fixture for this struct

The directive goes right above a struct type. Above any other declaration, such as type Role string, a type alias, or a function, nothing reads it, so it is an error.

-exclude does the same from the command line, for a package whose source you do not write, such as the models sqlc generates. A name it lists that the package does not declare gets a warning.

A struct marked //kanna:value gets no fixture either, and says nothing: it is a value type of kanna-value, whose one field is unexported and whose values are made through its constructor. A field of such a type is made with that constructor, of what a field named after the type would hold: orPanic(model.NewEmail(fmt.Sprintf("email-%d@example.com", n))) for an Email. A value the type's rule rejects makes the fixture panic, saying what to write instead: a fixture of the type written by hand, below. Only running tells, as the rule is the type's own code. Tag the field fake:"skip" to leave it zero instead. A field held to the type with rule=Type gets the same value, read through its accessor: orPanic(model.NewEmail(…)).String(). The fixtures cannot call the constructor of an unexported type, so a field held to one gets the value without it, and generation warns at the field that the type's rule may reject it.

Fixtures written by hand

A function the destination declares by hand, named after a type and returning it, is the fixture of the type, which the generated fixtures call for a field of it:

// fixture/nickname.go
func Nickname(f *gofakeit.Faker) model.Nickname {
	return model.MustNickname(strings.ToLower(f.FirstName()))
}

// fixture/user.go
func User(setters ...func(*model.User)) model.User {
	u := model.User{Name: "kanna"}
	for _, set := range setters {
		set(&u)
	}
	return u
}
Type Function
a struct, but a value type func T() model.T, or func T(setters ...func(*model.T)) model.T, as a generated fixture is
any other named type func T() model.T, or func T(f *gofakeit.Faker) model.T for the faker of the fixture calling it

This is also the way to fill a type kanna-fixture cannot, such as a struct of another package: func Inner() rules.Inner is called for every field of it, behind a pointer or in a slice too.

A fixture written by hand is called by the fixtures of its destination alone. Where the fixtures of two models go into two destinations, such as internal/model/fixture and internal/api/fixture, and both fill a field of the type, write it in each.

A struct with a fixture written by hand gets no generated fixture, and the others call this one instead, whether or not it is marked //kanna:fixture skip. Its faker is seeded as for a field tagged fake, with the number and the type of the fixture calling it, so the third User made gets the same nickname on every run. A value the function makes is not unique; count one of your own where it has to be:

var seqEmail atomic.Int64

func Email() model.Email {
	return model.MustEmail(fmt.Sprintf("user-%d@corp.example", seqEmail.Add(1)))
}

Another declaration of the name a generated fixture takes stops generation, saying how to declare it to be the fixture. What a fixture written by hand calls is up to its author, who has to keep it from coming back to the fixture that calls it.

The constants of a type other than an enum of string, such as time.Duration or an enum of int, are no choices a fixture picks from: it converts a number, model.Status(n). Write the fixture of the type for a value of your choosing:

// fixture/status.go
func Status() model.Status {
	return model.StatusActive
}

Skipped with a warning: a generic struct, for which a fixture has no type arguments to pick, and a struct that must not be copied, such as one holding a sync.Mutex, since a fixture returns a value. A protobuf message is one of these, as go vet would say of copying it. A misspelled directive also gets a warning, which would otherwise leave the struct with a fixture and say nothing, as does one a blank line keeps from its struct, and //kanna:fixture with an argument other than skip, or none, an error:

./model.go:3:1: warning: "//kanna:fixtur skip" is not recognized as a directive
  did you mean "//kanna:fixture skip"?
./model.go:8:1: error: //kanna:fixture takes skip, not "skp"
  did you mean "skip"?

When no struct is left, generation warns and removes the fixture_gen.go it wrote before.

Flags

kanna-fixture [flags] [package]
Flag Meaning
-destination <dir> write fixture_gen.go into dir (default fixture), a dir of its own for each model package
-exclude <names> comma-separated names of structs to skip
-tags <list> comma-separated build tags
-check verify the output is up to date instead of writing it, including that no output is left over; KANNA_CHECK=1 does the same
-v list the file written, removed, or left unchanged
-deps print what a run reads and writes as JSON, for the kanna command, and exit
-version print the version

The package defaults to the one in the current directory, so //go:generate go tool kanna-fixture in the model needs no argument and writes fixture/ beside its files; //go:generate go tool kanna-fixture ./model at the root of the module writes fixture/ there. Paths are relative to the directory go generate runs in, the one holding the //go:generate line.

The destination is a package other than the model, which its fixtures import, and holds the fixtures of that one package alone: a run stops at a fixture_gen.go there that holds those of another package, which it would overwrite. It may hold hand-written files too: their package is the one the generated file joins, which is named after the directory without them: write a doc.go there, package fixtures, for another name. A fixture whose name they already declare stops generation. They may call the fixtures before the first run has written them: kanna-fixture type-checks them once more with what it is about to write in place, and writes nothing while they do not compile with it, so a mistake a missing fixture hid is reported then, and a fixture they still call is not removed. Tests in other packages are not loaded, so go vet tells of a fixture they call that is gone. A run that succeeds prints nothing but its warnings. A file named fixture_gen.go without the generated-code header is never touched.

Example

example generates fixtures for a small blog model and a sign-up request (numbered values, constants, templates, and a pattern), checks with kanna-validator that the sign-up fixture is valid as made, and prints a few. CI regenerates it and fails if the output would change.

Development

make test       # unit tests
make lint       # golangci-lint
make example    # regenerate the example, then build and run it

License

MIT

Directories

Path Synopsis
cmd
kanna-fixture command
Command kanna-fixture generates a fixture for every exported struct of a package: a function returning the struct with every field it can fill set.
Command kanna-fixture generates a fixture for every exported struct of a package: a function returning the struct with every field it can fill set.
internal
gen
Package gen generates a fixture for every exported struct of a package: a function returning the struct with every field it can fill set.
Package gen generates a fixture for every exported struct of a package: a function returning the struct with every field it can fill set.

Jump to

Keyboard shortcuts

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