Skip to content

πŸ›  A test fixtures replacement for Go, support struct and ent, inspired by factory_bot/factory_boy

License

Notifications You must be signed in to change notification settings

Yiling-J/carrier

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

62 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

carrier - A Test Fixtures Replacement for Go

example workflow Go Report Card

  • Statically Typed - 100% statically typed using code generation
  • Developer Friendly API - explicit API with method chaining support
  • Feature Rich - Default/Sequence/SubFactory/PostHook/Trait
  • Ent Support - ent: An Entity Framework For Go

A snippet show how carrier works:

  • You have a model
type User struct {
	Name  string
	Email string
	Group *Group
}
  • Add carrier schema
Schemas := []carrier.Schema{
	&carrier.StructSchema{
		To: model.User{},
	},
}
  • Generate Structs πŸŽ‰
userMetaFactory := carrier.UserMetaFactory()
userFactory := userMetaFactory.
	SetNameDefault("carrier").
	SetEmailLazy(func(ctx context.Context, i *model.User) (string, error) {
		return fmt.Sprintf("%[email protected]", i.Name), nil
	}).
	SetGroupFactory(groupFactory.Create).
	Build()
user, err := userFactory.Create(ctx)
users, err := userFactory.CreateBatch(ctx, 5)

Installation

go get github.com/Yiling-J/carrier/cmd

After installing carrier codegen, go to the root directory(or the directory you think carrier should stay) of your project, and run:

go run github.com/Yiling-J/carrier/cmd init

The command above will generate carrier directory under current directory:

└── carrier
    └── schema
        └── schema.go

It's up to you where the carrier directory should be, just remember to use the right directory in MetaFactory Generation step.

Add Schema

Edit schema.go and add some schemas:

> struct

package schema

import (
	"github.com/Yiling-J/carrier"
)

var (
	Schemas = []carrier.Schema{
		&carrier.StructSchema{
			To: model.User{},
		},
	}
)

> ent

To support ent, you need to provide the ent.{Name}Create struct to schema, so carrier can get enough information.

package schema

import (
	"github.com/Yiling-J/carrier"
	"your/ent"
)

var (
	Schemas = []carrier.Schema{
		&carrier.EntSchema{
			To: &ent.UserCreate{},
		},
	}
)

The To field only accept struct/struct pointer, carrier will valid that on generation step. Schema definition reference

MetaFactory Generation

Run code generation from the root directory of the project as follows:

# this will use default schema path ./carrier/schema
go run github.com/Yiling-J/carrier/cmd generate

Or can use custom schema path:

go run github.com/Yiling-J/carrier/cmd generate ./your/carrier/schema

This produces the following files:

└── carrier
 Β Β  β”œβ”€β”€ factory
    β”‚   β”œβ”€β”€ base.go
    β”‚   β”œβ”€β”€ ent_user.go
 Β Β  β”‚Β Β  └── user.go
    β”œβ”€β”€ schema
    β”‚Β Β  └── schema.go
    └── factory.go

Here factory.go include all meta factories you need. Also all ent files and meta factories will have ent prefix to avoid name conflict.

If you update schemas, just run generate again.

Build Factory and Generate Structs

To construct a real factory for testing:

Create MetaFactory struct

userMetaFactory := carrier.UserMetaFactory()

Build factory from meta factory

userFactory := userMetaFactory.SetNameDefault("carrier").Build()

MetaFactory provide several methods to help you initial field values automatically.

MetaFactory API Reference

Create structs

> struct

user, err := userFactory.Create(context.TODO())
users, err := userFactory.CreateBatch(context.TODO(), 3)

> ent

// need ent client
user, err := userFactory.Client(entClient).Create(context.TODO())
user, err := userFactory.Client(entClient).CreateBatch(context.TODO(), 3)

Factory API Reference

Use factory wrapper

Carrier also include a wrapper where you can put all your factories in:

> struct

factory := carrier.NewFactory()
factory.SetUserFactory(userFactory)
factory.UserFactory().Create(context.TODO())

> ent

factory := carrier.NewEntFactory(client)
// this step will assign factory client to userFactory also
factory.SetUserFactory(userFactory)
factory.UserFactory().Create(context.TODO())
// access ent client
client := factory.Client()

Schema Definition

There are 2 kinds of schemas StructSchema and EntSchema, both of them implement carrier.Schema interface so you can put them in the schema slice.

Each schema has 4 fields:

  • Alias: Optional. If you have 2 struct type from different package, but have same name, add alias for them. Carrier will use alias directly as factory name.

  • To: Required. For StructSchema, this is the struct factory should generate. Carrier will get struct type from it and used in code generation, Only public fields are concerned. For EntSchema, this field should be the {SchemaName}Create struct which ent generated. Carrier will look up all Set{Field} methods and generate factory based on them. Both struct and pointer of struct are OK.

  • Traits: Optional. String slice of trait names. Traits allow you to group attributes together and override them at once.

  • Posts: Optional. Slice of carrier.PostField. Each carrier.PostField require Name(string) and Input(any interface{}), and map to a post function after code generation. Post function will run after struct created, with input value as param.

MetaFactory API

MetaFactory API can be categorized into 8 types of method:

  • Each field in To struct has 4 types:

    • Default: Set{Field}Default
    • Sequence: Set{Field}Sequence
    • Lazy: Set{Field}Lazy
    • Factory: Set{Field}Factory
  • Each field in []Posts has 1 type:

    • Post: Set{PostField}PostFunc
  • Each name in []Traits has 1 type:

    • Trait: Set{TraitName}Trait
  • Each MetaFactory has 2 type:

    • BeforeCreate: SetBeforeCreateFunc
    • AfterCreate: SetAfterCreateFunc

The evaluation order of these methods are:

Trait -> Default/Sequence/Factory -> Lazy -> BeforeCreate -> Create -> AfterCreate -> Post

Create only exists in ent factory, will call ent builder Save method.

Put Trait first because Trait can override other types.

All methods except Default and Trait use function as input and it's fine to set it to nil. This is very useful in Trait override.

Default

Set a fixed default value for field.

userMetaFactory.SetNameDefault("carrier")

Sequence

If a field should be unique, and thus different for all built structs, use a sequence. Sequence counter is shared by all fields in a factory, not a single field.

// i is the current sequence counter
userMetaFactory.SetNameSequence(
	func(ctx context.Context, i int) (string, error) {
		return fmt.Sprintf("user_%d", i), nil
	},
),

The sequence counter is concurrent safe and increase by 1 each time factory's Create method called.

Lazy

For fields whose value is computed from other fields, use lazy attribute. Only Default/Sequence/Factory values are accessible in the struct.

userMetaFactory.SetEmailLazy(
	func(ctx context.Context, i *model.User) (string, error) {
		return fmt.Sprintf("%[email protected]", i.Name), nil
	},
)

> ent

Ent is a little different because the struct is created after Save. And carrier call ent's Set{Field} method to set values. So the input param here is not *model.User, but a temp containter struct created by carrier, hold all fields you can set.

entUserMetaFactory.SetEmailLazy(
	func(ctx context.Context, i *factory.EntUserMutator) (string, error) {
		return fmt.Sprintf("%[email protected]", i.Name), nil
	},
)

You can get original ent mutation using i.EntCreator():

entUserMetaFactory.SetEmailLazy(
	func(ctx context.Context, i *factory.EntUserMutator) (string, error) {
		i.EntCreator().SetName("apple")
		return "[email protected]", nil
	},
)

Factory

If a field's value has related factory, use relatedFactory.Create method as param here. You can also set the function manually.

// User struct has a Group field, type is Group
userMetaFactory.SetGroupFactory(groupFactory.Create)

> ent

Make sure related factory's ent client is set. By using factory wrapper or set it explicitly.

BeforeCreate

For struct factory, before create function is called after all lazy functions done. For ent factory, before create function is called right before to ent's Save method.

groupMetaFactory.SetBeforeCreateFunc(func(ctx context.Context, i *model.User) error {
	return nil
})

> ent

entUserMetaFactory.SetBeforeCreateFunc(func(ctx context.Context, i *factory.EntUserMutator) error {
	return nil
})

AfterCreate

For struct factory, after create function is called after all lazy functions done. For ent factory, after create function is called next to ent's Save method.

userMetaFactory.SetAfterCreateFunc(func(ctx context.Context, i *model.User) error {
	fmt.Printf("user: %d saved", i.Name)
	return nil
})

Post

Post functions will run once AfterCreate step done.

// user MetaFactory
userMetaFactory.SetWelcomePostFunc(
	func(ctx context.Context, set bool, obj *model.User, i string) error {
		if set {
			message.SendTo(obj, i)
		}
		return nil
	},
)
// user Factory, send welcome message
userFactory.SetWelcomePost("welcome to carrier").Create(context.TODO())
// user Factory, no welcome message
userFactory.Create(context.TODO())

Trait

Trait is used to override some fields at once, activated by With{Name}Trait method.

// override name
userMetaFactory.SetGopherTrait(factory.UserTrait().SetNameDefault("gopher"))
// user Factory
userFactory.WithGopherTrait().Create(context.TODO())

The Trait struct share same API with MetaFactory except Set{Name}Trait one, that means you can override 6 methods within a trait. Trait only override methods you explicitly set, the exampe above will only override name field. So you can combine multiple traits together, each change some parts of the struct. If multiple traits override same field, the last one will win:

userMetaFactory.SetGopherTrait(factory.UserTrait().SetNameDefault("gopher")).
SetFooTrait(factory.UserTrait().SetNameDefault("foo"))
// user name is foo
userFactory.WithGopherTrait().WithFooTrait().Create(context.TODO())
// user name is gopher
userFactory.WithFooTrait().WithGopherTrait().Create(context.TODO())

Build

This is the final step for MetaFactory definition, call this method will return a Factory which you can use to create structs.

Factory API

Factory API provide 3 types of method, Set{Field} to override some field, Set{Field}Post to call post function and With{Name}Trait to enable trait.

Set

Override field value. This method has the highest priority and will override your field method in MetaFactory.

userFactory.SetName("foo").Create(context.TODO())

SetPost

Call post function defined in MetaFactory with param.

// create a user with 3 friends
userFactory.SetFriendsPost(3).Create(context.TODO())

WithTrait

Enable a named trait. If you enable multi traits, and traits have overlapping, the latter one will override the former.

userFactory.WithFooTrait().WithBarTrait().Create(context.TODO())

Create

Create pointer of struct.

CreateV

Create struct.

CreateBatch

Create slice of struct pointer.

CreateBatchV

Create slice of struct.

Common Recipes

About

πŸ›  A test fixtures replacement for Go, support struct and ent, inspired by factory_bot/factory_boy

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages