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.26.
Install
go get -tool github.com/go-kanna/fixture/cmd/kanna-fixtureThe 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.
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 |
| a named 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, or zero |
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 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.
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.
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-validate 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 |
min, max, len on strings and slices |
a length within them: password-001 for min=12, k elements for min=k
|
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, or where it rules out part of a range, the lowest value 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-validate has |
a string gofakeit generates from the expression |
dive |
the rules after it, for each element made |
The rules are read to be satisfied, not checked: a rule kanna-fixture does not know is left to whatever validates the
struct, and the value it gets may not pass it. 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 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/.
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
{uuid}. 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.
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. 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)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 |
-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:
orZero(model.NewEmail(fmt.Sprintf("email-%d@example.com", n))) for an Email. A value the type's rule rejects leaves
the zero value, as one never given, rather than a panic, since a fixture is made before its setters run and a setter
could not get past one; set such a field with MustX in a setter.
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, 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, got "skp"
did you mean "//kanna:fixture 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) |
-package <name> |
call the package name when the destination has no Go files yet (default: the directory's name) |
-exclude <names> |
comma-separated names of structs to skip |
-check |
verify the output is up to date instead of writing it, including that no output is left over |
-v |
list the file written, removed, or left unchanged |
-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. It may hold hand-written files too: their
package is the one the generated file joins, and -package naming another is an error; 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-validate 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