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

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{
		{
			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{
		{
			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 7 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 1 type:

    • AfterCreate: SetAfterCreateFunc

The evaluation order of these methods are:

Trait -> Default/Sequence/Factory -> Lazy -> 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
	},
)

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.

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

Similar Resources

Extensible network application framework inspired by netty

GO-NETTY 中文介绍 Introduction go-netty is heavily inspired by netty Feature Extensible transport support, default support TCP, UDP, QUIC, KCP, Websocket

Dec 28, 2022

An easy HTTP client for Go. Inspired by the immortal Requests.

rek An easy HTTP client for Go inspired by Requests, plus all the Go-specific goodies you'd hope for in a client. Here's an example: // GET request re

Sep 20, 2022

Inspired by Have I Been Pwnd

Inspired by Have I Been Pwnd

Have I Been Redised Check it out at RedisPwned and Have I Been Redised How it works We scan the internet for exposed Redis databases broadcasting to t

Nov 14, 2021

Advanced Network Pinger inspired by paping.

Advanced Network Pinger inspired by paping.

itcp Advanced Network Pinger with various options. Why use itcp? TCP and ICMP support Hostname Lookup Threads Timeout ISP lookup Small showcase of itc

Jun 26, 2022

MPD client inspired by ncmpcpp written in GO with builtin cover art previews.

 MPD client inspired by ncmpcpp written in GO with builtin cover art previews.

goMP MPD client inspired by ncmpcpp written in GO demo.mp4 Roadmap Add Functionality to Sort out most played songs Add a config parser Image Previews

Jan 1, 2023

USENET-inspired decentralized internet discussion system

USENET-inspired decentralized internet discussion system

===============================================================================

Jan 6, 2023

A little tool to test IP addresses quickly against a geolocation and a reputation API

iptester A little tool to test IP addresses quickly against a geolocation and a

May 19, 2022

gRPC LRU-cache server and client with load test

gRPC k-v storage with LRU-cache server & client + load test. Specify LRU-cache capacity: server/cmd/app.go - StorageCapacity go build ./server/cmd/*

Dec 26, 2021
Comments
  • Create an ent schema with invalid `creator` data type

    Create an ent schema with invalid `creator` data type

    When creating carrier with ent schema, it generates factories with invalid data type since the data type is hardcoded in the template here https://github.com/Yiling-J/carrier/blob/master/template/carrier.tmpl#L32,

    {{$DynamicParams = $RawSchemaName | printf "creator *ent.%sCreate"}}
    

    which generates

    func(ctx context.Context, i *EntUserMutator, c int, creator *ent.UserCreate)
    

    and in my case it should be like this

    func(ctx context.Context, i *EntUserMutator, c int, creator *model.UserCreate)
    

    The generation should be generic to whatever the ent directory the user has, in my case I call it model not ent

    β”œβ”€β”€ db/
    β”‚   β”œβ”€β”€ model/     # ent generated files
    β”‚   β”‚    └── user/
    β”‚   β”‚        └── user.go
    β”‚   └── carrier/
    β”‚           └── schema.go
    
  • Ent: missing required edge

    Ent: missing required edge

    I would like to be able to add edges to an ent-factory before the item is created. Because in my ent.schema definition, I have declared several required (M2M) edges. When trying to create an item without these edges, the ent-validation fails.

    Error: ent: missing required edge "Team.members"

    Ent schema

    type Team struct {
    	ent.Schema
    }
    
    func (Team) Edges() []ent.Edge {
    	return []ent.Edge{
    		edge.To("members", Player.Type).
    			Required(),
    	}
    }
    

    Carrier schema

    		&carrier.EntSchema{
    			To: &ent.TeamCreate{},
    		},
    

    Is it possible to add a Add{Edge}Func which is called before the item is created or maybe a method which is called before the creation with a ent.{Type}Create parameter to intercept the creation and add the required edges.

    Example Method-Signature:

    type fn func(context.Context, *ent.TeamCreate) (*ent.TeamCreate, error)
    

    Thanks

Feb 10, 2022
Zero downtime restarts for go servers (Drop in replacement for http.ListenAndServe)

endless Zero downtime restarts for golang HTTP and HTTPS servers. (for golang 1.3+) Inspiration & Credits Well... it's what you want right - no need t

Dec 29, 2022
Drop-in replacement for Go net/http when running in AWS Lambda & API Gateway
Drop-in replacement for Go net/http when running in AWS Lambda & API Gateway

Package gateway provides a drop-in replacement for net/http's ListenAndServe for use in AWS Lambda & API Gateway, simply swap it out for gateway.Liste

Nov 24, 2022
Podbit is a replacement for newsboat's standard podboat tool for listening to podcasts.

Podbit - Podboat Improved Podbit is a replacement for newsboat's standard podboat tool for listening to podcasts. It is minimal, performant and abides

Dec 8, 2022
A minimal IPFS replacement for P2P IPLD apps

IPFS-Nucleus IPFS-Nucleus is a minimal block daemon for IPLD based services. You could call it an IPLDaemon. It implements the following http api call

Jan 4, 2023
PlanB: a HTTP and websocket proxy backed by Redis and inspired by Hipache.

PlanB: a distributed HTTP and websocket proxy What Is It? PlanB is a HTTP and websocket proxy backed by Redis and inspired by Hipache. It aims to be f

Mar 20, 2022
Inspired by go-socks5,This package provides full functionality of socks5 protocol.
Inspired by go-socks5,This package provides full functionality of socks5 protocol.

The protocol described here is designed to provide a framework for client-server applications in both the TCP and UDP domains to conveniently and securely use the services of a network firewall.

Dec 16, 2022
A terminal UI for tshark, inspired by Wireshark
A terminal UI for tshark, inspired by Wireshark

Termshark A terminal user-interface for tshark, inspired by Wireshark. V2.2 is out now with vim keys, packet marks, a command-line and themes! See the

Jan 9, 2023