factory

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Jan 15, 2018 License: Apache-2.0 Imports: 8 Imported by: 0

README

Factory: Factory for Go Tests

Build Status GoDoc Go Report Card

Factory is a fixtures replacement. With its readable APIs, you can define factories and use factories to create saved and unsaved by build multiple strategies.

Factory's APIs are inspired by factory_bot in Ruby.

See how easily to use factory:

import (
	. "github.com/nauyey/factory"
	"github.com/nauyey/factory/def"
)

type User struct {
	ID        int64     `factory:"id,primary"`
	Name      string    `factory:"name"`
	Gender    string    `factory:"gender"`
	Email     string    `factory:"email"`
}

// Define a factory for User struct
userFactory := def.NewFactory(User{}, "db_table_users",
	def.SequenceField("ID", 1, func(n int64) interface{} {
		return n
	}),
	def.DynamicField("Name", func(user interface{}) (interface{}, error) {
		return fmt.Sprintf("User Name %d", user.(*User).ID), nil
	}),
	def.Trait("boy",
		def.Field("Gender", "male"),
	),
)

user := &User{}
Build(userFactory).To(user)
// user.ID   -> 1
// user.Name -> "User Name 1"

user2 := &User{}
Create(userFactory, WithTraits("boy")).To(user2) // saved to database
// user2.ID      -> 2
// user2.Name   -> "User Name 2"
// user2.Gender -> "male"

Table of Contents


Feature Support

  • Fields
  • Dynamic Fields
  • Dependent Fields
  • Sequence Fields
  • Multilevel Fields
  • Associations
  • Traits
  • Callbacks
  • Multiple Build Strategies

Installation

Simple install the package to your $GOPATH with the go tool from shell:

$ go get -u github.com/nauyey/factory

Usage

Before use factory to create the target test data, you should define a factory of the target struct.

  1. Use APIs from sub package github.com/nauyey/factory/def to define factories.
  2. Use APIs from package github.com/nauyey/factory to build the target test data.
Defining factories

Each factory is a def.Factory instance which is related to a specific golang struct and has a set of fields. For factory to create saved instance, the database table name is also needed:

import "github.com/nauyey/factory/def"

type User struct {
	ID        int64
	Name      string
	Gender    string
	Age       int
	BirthTime time.Time
	Country   string
	Email     string
}

// This will define a factory for User struct
userFactory := def.NewFactory(User{}, "",
	def.Field("Name", "test name"),
	def.SequenceField("ID", 0, func(n int64) interface{} {
		return n
	}),
	def.Trait("Chinese",
		def.Field("Country", "china"),
	),
	def.AfterBuild(func(user interface{}) error {
		// do something
	}),
)

type UserForSave struct {
	ID        int64     `factory:"id,primary"`
	Name      string    `factory:"name"`
	Gender    string    `factory:"gender"`
	Age       int       `factory:"age"`
	BirthTime time.Time `factory:"birth_time`
	Country   string    `factory:"country"`
	Email     string    `factory:"email"`
}

// This will define a factory for UserForSave struct with database table
userForSaveFactory := def.NewFactory(UserForSave{}, "model_table",
	def.Field("Name", "test name"),
	def.SequenceField("ID", 0, func(n int64) interface{} {
		return n
	}),
	def.Trait("Chinese",
		def.Field("Country", "china"),
	),
	def.BeforeCreate(func(user interface{}) error {
		// do something
	}),
	def.AfterCreate(func(user interface{}) error {
		// do something
	}),
)

For factory to create saved instance, the struct fields will be mapped to database table fields by tags declared in the origianl struct. Tag name is factory. And the mapping rules are as following:

  1. If a struct field, like ID, has tag factory:"id", then the field will be map to be the field "id" in database table.
  2. If a struct field, like ID, has tag factory:"id,primary", then the field will be map to table field "id", and factory will treat it as the primary key of the table.
  3. If a struct field, like NickName, has tag factory:"", factory:",", factory:",primary" or factory:",anything else", then the field will be map to the table field named "nick_name". In this situation, factory just use the snake case of the original struct field name as table field name.

It is highly recommended that you have one factory for each struct that provides the simplest set of fields necessary to create an instance of that struct.

For different kinds of scenarios, you can define different traits for them.

Using factories

factory supports several different build strategies: Build, BuildSlice, Create, CreateSlice, Delete:

import . "github.com/nauyey/factory"

// Returns a user instance that's not saved
user := &User{}
err := Build(userFactory).To(user)

// Returns a saved User instance
user := &User{}
err := Create(userFactory).To(user)

// Deletes a saved User instance from database
err := Delete(userFactory, user)

No matter which strategy is used, it's possible to override the defined fields by passing factoryOption type of parameters. Currently, factory supports WithTraits, WithField:

import . "github.com/nauyey/factory"

// Build a User instance and override the name field
user := &User{}
err := Build(userFactory, WithField("Name", "Tony")).To(user)
// user.Name => "Tony"

// Build a User instance with traits
user := &User{}
err := Build(userFactory, 
	WithTraits("Chinese"), 
	WithField("Name", "XiaoMing"),
).To(user)
// user.Name => "XiaoMing"
// user.Country => "China"

Before using Create, CreateSlice and Delete, a *sql.DB instance should be seted to factory:

import "github.com/nauyey/factory"

var db *sql.DB

// init a *sql.DB instance to db

factory.SetDB(db)
Fields

def.Field sets struct field values:

import "github.com/nauyey/factory/def"

type Blog struct {
	ID       int64  `factory:"id,primary"`
	Title    string `factory:"title"`
	Content  string `factory:"content"`
	AuthorID int64  `factory:"author_id"`
	Author   *User
}

blogFactory := def.NewFactory(Blog{}, "",
	def.Field("Title", "blog title"),
)
blog := &Blog{}
err := Build(blogFactory).To(blog)
// blog.Title => "blog title"
Dynamic Fields

Most factory fields can be added using static values that are evaluated when the factory is defined, but some fields (such as associations and other fields that must be dynamically generated) will need values assigned each time an instance is generated. These "dynamic" fields can be added by passing a DynamicFieldValue type generator function to DynamicField instead of a parameter:

import (
	"time"

	"github.com/nauyey/factory/def"
)

userFactory := def.NewFactory(User{}, "model_table",
	def.Field("Name", "test name"),
	def.DynamicField("Age", func(model interface{}) interface{} {
		now := time.Now()
		birthTime, _ := time.Parse("2006-01-02T15:04:05.000Z", "2017-11-19T00:00:00.000Z")
		return birthTime.Sub(now).Years()
	}),
)
Dependent Fields

Fields can be based on the values of other fields using the evaluator that is yielded to dynamic field value generator function:

import (
	"time"

	"github.com/nauyey/factory/def"
)

userFactory := def.NewFactory(User{}, "model_table",
	def.Field("Name", "test name"),
	def.DynamicField("Age", func(model interface{}) (interface{}, error) {
		user, ok := model.(*User)
		if !ok {
			return nil, errors.NewFactory("invalid type of model in DynamicFieldValue function")
		}
		now := time.Now()
		return user.BirthTime.Sub(now).Years()
	}),
)
Sequence Fields

Unique values in a specific format (for example, e-mail addresses) can be generated using sequences. Sequence fields are defined by calling SequenceField in factory model defination, and values in a sequence are generated by calling SequenceFieldValue type of callback function:

import (
	. "github.com/nauyey/factory"
	"github.com/nauyey/factory/def"
)

// Defines a new sequence field
userFactory := def.NewFactory(User{}, "model_table",
	def.SequenceField("Email", 0, func(n int64) interface{} {
		return fmt.Sprintf("person%d@example.com", n)
	}),
)

user0 := &User{}
err := Build(userFactory).To(user0)
// user0.Email => "person0@example.com"

user1 := &User{}
err := Build(userFactory).To(user1)
// user1.Email => "person1@example.com"

You can also set the initial start of the sequence:

userFactory := def.NewFactory(User{}, "model_table",
	def.SequenceField("Email", 1000, func(n int64) interface{} {
		return fmt.Sprintf("person%d@example.com", n)
	}),
)

user0 := &User{}
err := Build(userFactory).To(user0)
// user0.Email => "person1000@example.com"
Multilevel Fields

Multilevel fields feature supplies a way to set nested struct field values:

import "github.com/nauyey/factory/def"

type Blog struct {
	ID       int64  `factory:"id,primary"`
	Title    string `factory:"title"`
	Content  string `factory:"content"`
	AuthorID int64  `factory:"author_id"`
	Author   *User
}

// Author.Name is a multilevel field
blogFactory := def.NewFactory(Blog{}, "",
	def.Field("Title", "blog title"),
	def.Field("Author.Name", "blog author name"),
)
blog := &Blog{}
err := Build(blogFactory).To(blog)
// blog.Title => "blog title"
// blog.Author.Name => "blog author name"
Associations

It's possible to set up associations within factories. Use def.Association to define an association of a factory by specify a different def. And you can override fields definitions of this association factory.

import "github.com/nauyey/factory/def"

type Blog struct {
	ID       int64  `factory:"id,primary"`
	Title    string `factory:"title"`
	Content  string `factory:"content"`
	AuthorID int64  `factory:"author_id"`
	Author   *User
}

userFactory := def.NewFactory(User{}, "user_table",
	def.Field("Name", "test name"),
)

blogFactory := def.NewFactory(Blog{}, "blog_table",
	// define an association
	def.Association("Author", "AuthorID", "ID", userFactory,
		def.Field("Name", "blog author name"), // override field
	),
)

In factory, there isn't a direct way to define one-to-many relationships. But you can define a one-to-many relationships in def.AfterBuild and def.AfterCreate:

import (
	. "github.com/nauyey/factory"
	"github.com/nauyey/factory/def"
)

type User struct {
	ID    int64  `factory:"id,primary"`
	Name  string `factory:"name"`
	Blogs []*Blog
}

type Blog struct {
	ID       int64  `factory:"id,primary"`
	Title    string `factory:"title"`
	Content  string `factory:"content"`
	AuthorID int64  `factory:"author_id"`
	Author   *User
}

blogFactory := def.NewFactory(Blog{}, "blog_table",
	def.SequenceField("ID", 1, func(n int64) interface{} {
		return n
	}),
)

// define unsaved one-to-many associations in AfterBuild
userFactory := def.NewFactory(User{}, "",
	def.Field("Name", "test name"),
	def.Trait("with unsaved blogs",
		def.AfterBuild(func(user interface{}) error {
			author, _ := user.(*User)

			author.Blogs = []*Blog{}
			return BuildSlice(blogFactory, 10,
				WithField("AuthorID", author.ID),
				WithField("Author", author),
			).To(&author.Blogs)
		}),
	),
)

// define saved one-to-many associations in AfterCreate
userForSaveFactory := def.NewFactory(User{}, "user_table",
	def.Field("Name", "test name"),
	def.Trait("with saved blogs",
		def.AfterCreate(func(user interface{}) error {
			author, _ := user.(*User)
			
			author.Blogs = []*Blog{}
			return CreateSlice(blogFactory, 10,
				WithField("AuthorID", author.ID),
				WithField("Author", author),
			).To(&author.Blogs)
		}),
	),
)

The behavior of the def.Association function varies depending on the build strategy used for the parent object.

// Builds and saves a User and a Blog
blog := &Blog{}
err := Create(blogModel).To(blog) // blog is saved into database
user := blog.Author // user is saved into database


// Builds a User and a Blog, but saves nothing
blog := &Blog{}
err := Build(blogModel).To(blog) // blog isn't saved
user = blog.Author // user isn't saved
Trait

Trait allows you to group fields together and then apply them to the factory model.

import (
	. "github.com/nauyey/factory"
	"github.com/nauyey/factory/def"
)

userFactory := def.NewFactory(User{}, "",
	def.Field("Name", "Taylor"),
	def.Trait("Chinese boy",
		def.Field("Country", "China"),
		def.Field("Gender", "Male"),
	),
)

user := &User{}
err := Build(userFactory, WithTraits("Chinese boy")).To(user)
// user.Country => "China"
// user.Gender => "Male"

Traits that defines the same fields are allowed. Traits can also be passed in as a slice of strings, by using WithTraits, when you construct an instance from factory.The fields that defined in the latest trait gets precedence.

import (
	. "github.com/nauyey/factory"
	"github.com/nauyey/factory/def"
)

userFactory := def.NewFactory(User{}, "",
	def.Field("Name", "Taylor"),
	def.Trait("Chinese boy",
		def.Field("Country", "China"),
		def.Field("Gender", "Male"),
	),
	def.Trait("American",
		def.Field("Country", "USA"),
	),
	def.Trait("girl",
		def.Field("Gender", "Female"),
	),
)

user := &User{}
err := Build(userFactory, WithTraits("Chinese boy", "American", "girl")).To(user)
// user.Country => "USA"
// user.Gender => "Female"

This ability works with build and create.

Traits can be used with associations easily too:

import "github.com/nauyey/factory/def"

blogFactory := def.NewFactory(Blog{}, "blog_table",
	// define an association in traits
	def.Trait("with author",
		def.Association("Author", "AuthorID", "ID", userFactory,
			def.Field("Name", "blog author in trait"), // override field
		),
	),
)

blog := &Blog{}
err := Build(blogFactory, WithTraits("with author")).To(blog)
// blog.Author
// blog.Author.Name => "blog author in trait"

Traits cann't be used within other traits.

Callbacks

factory makes available 3 callbacks for injections:

  • def.AfterBuild - called after an instance is built (via Build, Create)
  • def.BeforeCreate - called before an instance is saved (via Create)
  • def.AfterCreate - called after an instance is saved (via Create)

Examples:

import "github.com/nauyey/factory/def"

// Define a factory that calls the callback function after it is built
userFactory := def.NewFactory(User{}, "",
	def.AfterBuild(func(user interface{}) error {
		// do something
	}),
)

Note that you'll have an instance of the user in the callback function. This can be useful.

You can also define multiple types of callbacks on the same factory:

import "github.com/nauyey/factory/def"

// Define a factory that calls the callback function after it is built
userFactory := def.NewFactory(User{}, "",
	def.AfterBuild(func(user interface{}) error {
		// do something
	}),
	def.BeforeCreate(func(user interface{}) error {
		// do something
	}),
	def.AfterCreate(func(user interface{}) error {
		// do something
	}),
)

Factories can also define any number of the same kind of callback. These callbacks will be executed in the order they are specified:

import "github.com/nauyey/factory/def"

// Define a factory that calls the callback function after it is built
userFactory := def.NewFactory(User{}, "",
	def.AfterBuild(func(user interface{}) error {
		// do something
	}),
	def.AfterBuild(func(user interface{}) error {
		// do something
	}),
	def.AfterBuild(func(user interface{}) error {
		// do something
	}),
)

Calling Create will invoke both def.AfterBuild and def.AfterCreate callbacks.

Building or Creating Multiple Records

Sometimes, you'll want to create or build multiple instances of a factory at once.

import . "github.com/nauyey/factory"

users := []*User{}

err := BuildSlice(userFactory, 10).To(users)
err := CreateSlice(userFactory, 10).To(users)

To set the fields for each of the factories, you can use WithField and WithTraits as you normally would.:

import . "github.com/nauyey/factory"

users := []*User{}

err := BuildSlice(userFactory, 10, WithField("Name", "build slice name")).To(users)

How to Contribute

  1. Check for open issues or open a fresh issue to start a discussion around a feature idea or a bug.
  2. Fork the repository on GitHub to start making your changes to the master branch (or branch off of it).
  3. Write a test which shows that the bug was fixed or that the feature works as expected.
  4. Send a pull request and bug the maintainer until it gets merged and published. :) Make sure to add yourself to AUTHORS.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var DebugMode = false

DebugMode is a flag controlling whether debug information is outputted to the os.Stdout

Functions

func Build

func Build(f *Factory, opts ...factoryOption) to

Build creates an instance from a factory but won't store it into database.

model := &Model{}

err := Build(FactoryModel,

WithTrait("Chinese"),
WithField("Name", "new name"),
WithField("ID", 123),

).To(model)

func BuildSlice

func BuildSlice(f *Factory, count int, opts ...factoryOption) to

BuildSlice creates a slice instance from a factory but won't store them into database.

modelSlice := []*Model{}

err := Build(FactoryModel,

WithTrait("Chinese"),
WithField("Name", "new name"),

).To(&modelSlice)

func Create

func Create(f *Factory, opts ...factoryOption) to

Create creates an instance from a factory and stores it into database.

model := &Model{}

err := Create(FactoryModel,

WithTrait("Chinese"),
WithField("Name", "new name"),
WithField("ID", 123),

).To(model)

func CreateSlice

func CreateSlice(f *Factory, count int, opts ...factoryOption) to

CreateSlice creates a slice of instance from a factory and stores them into database.

modelSlice := []*Model{}

err := CreateSlice(FactoryModel,

WithTrait("Chinese"),
WithField("Name", "new name"),

).To(&modelSlice)

func Delete

func Delete(f *Factory, instance interface{}) error

Delete deletes an instance of a factory model from database. Example: err := Delete(FactoryModel, Model{})

func SetDB

func SetDB(db *sql.DB)

SetDB sets database connection for factory

func WithField

func WithField(name string, value interface{}) factoryOption

WithField sets the value of a specific field. This way has the highest priority to set the field value.

func WithTraits

func WithTraits(traits ...string) factoryOption

WithTraits defines which traits the new instance will use. It can take multiple traits. These traits will be executed one by one. So the later one may override the one before.

For example:

The trait "trait1" set Field1 as "value1", and at the same time, trait "trait2" set Field1 as "value2". The WithTraits("trait1", "trait2") will finally set Field1 as "value2".

Types

type AssociationFieldValue

type AssociationFieldValue struct {
	ReferenceField            string
	AssociationReferenceField string
	OriginalFactory           *Factory
	Factory                   *Factory
}

AssociationFieldValue represents a struct which contains data to generate value of a association field.

type Callback

type Callback func(model interface{}) error

Callback defines the callback function type

type DynamicFieldValue

type DynamicFieldValue func(model interface{}) (interface{}, error)

DynamicFieldValue defines the value generator type of a field. It's return result will be set as the value of the field dynamicly.

type Factory

type Factory struct {
	ModelType              reflect.Type
	Table                  string
	FiledValues            map[string]interface{}
	SequenceFiledValues    map[string]*sequenceValue
	DynamicFieldValues     map[string]DynamicFieldValue
	AssociationFieldValues map[string]*AssociationFieldValue
	Traits                 map[string]*Factory
	AfterBuildCallbacks    []Callback
	BeforeCreateCallbacks  []Callback
	AfterCreateCallbacks   []Callback

	CanHaveAssociations bool
	CanHaveTraits       bool
	CanHaveCallbacks    bool
}

Factory represents a factory defined by some model struct

func (*Factory) AddSequenceFiledValue

func (f *Factory) AddSequenceFiledValue(name string, first int64, value SequenceFieldValue)

AddSequenceFiledValue adds sequence field value to factory by field name

type SequenceFieldValue

type SequenceFieldValue func(n int64) (interface{}, error)

SequenceFieldValue defines the value generator type of sequence field. It's return result will be set as the value of the sequence field dynamicly.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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