README
¶
fixture
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
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. |