testfuse - Guide to usage of library fuse
Simple project to demonstrate usage of the fuse and mock packages.
These packages are available at the repo -
https://github.com/rvauradkar1/fuse
The following component graph is used to demonstrate the usage of
fuse/mock.
Please review the package structure of this project - folders and
sub-folders are provided to demonstrate wiring of components and
generation of mocks to aid unit-testing.
A graph of the components is provided below.
- The vertices represent components.
- The edges represent dependencies.
- The labels represent state.
API for fuse:
// Fuse is used by clients to configure dependency injection (DI)
type Fuse interface {
// Register a slice of components
Register(entries []Entry) []error
// Wire injects dependencies into components.
Wire() []error
// Find is needed only for stateful components. Can also be used for stateless in case dependencies are not defined at instance level
Find(name string) interface{}
}
Steps to follow:
- Declare components and configure dependencies
- Create a config package to avoid cyclic dependencies
- Create a slice of component entries
- Register and fuse the components (Dependency Injection pattern)
- Provide a finder to find stateful components (Resource Locator pattern)
Dependencies are configured using struct
tags.
Example 1 - Stateless dependencies: The code below defines a struct
. This component depends on 2 other components (look at
the component graph above)
- CartSvc (pointer variable) - tagged with key _fuse and the name of
the component. - AuthSvc (interface variable) - tagged with key _fuse and the name of
the component.
type OrderController struct {
CartSvc *cart.CartSvc `_fuse:"CartSvc"`
AuthSvc auth.IService `_fuse:"AuthSvc"`
}
Example 2 - Stateful dependencies: The code below defines a struct
.
- DEPS_ interface{} - tagged with key _deps and a comma-delimited string of stateful dependencies. These are not mandatory for DI but are useful during mock code generation.
type CartSvc struct {
DEPS_ interface{} `_deps:"OrderSvc"`
}
Note: Config package is needed for several reasons:
- Application packages cannot refer to 'main'.
- Go does not allow cyclical references.
- Application packages do not need to import 'fuse', they will import
'cfg' and use just the 'find' method. Eases refactoring if projects
want to discontinue usage of 'fuse'.
Following diagrams shows a high-level dependency:
- Application package 'main' depends on 'cfg' and 'fuse'. 'main'
provides 'cfg' with the list of component entries. - 'cfg' uses 'fuse', register and fuses the components together. The
'find' method from the 'fuse' package is assigned to the 'find'
method from the 'cfg' package. Other packages use this method without
importing 'fuse' - 'other packages' are all other application packages. They depend on
'cfg' and use only the 'find' method.
The component slice is created in the application 'main' package. Helps
in:
- Isolating dependencies on the 'fuse' package.
- One place to define the graph(s).
- Multiple slices can be created in case of different component graphs.
- 'mock' package uses these slices to create mock for unit-testing.
Example:
The code below is defined in the application main file. It can be named
anything. This same method will be used by the mock generator. Each
component entry is recorded as a fuse.Entry with properties:
- Name : Unique identifier for the component. This same identifier is
used when defining the '_fuse' dependency. - State : Stateful or stateless. If stateless, the component instance
provided is always used, no new instances are created. If stateful, a
new instance is created for each use. If this component has
dependencies on other stateless components, they are recursively
populated as well. - Instance : A pointer to the
struct
, if it is not a pointer
variable, an erros is generated during registry process.
func main() {
fmt.Println("Hello testfuse")
entries := Entries()
errors := cfg.Fuse(entries)
..........
}
func Entries() []fuse.Entry {
fmt.Println("Hello testfuse")
entries := make([]fuse.Entry, 0)
entries = append(entries, fuse.Entry{Name: "OrdCtrl", State: false, Instance: &ctrl.OrderController{}})
entries = append(entries, fuse.Entry{Name: "CartSvc", State: false, Instance: &cart.CartSvc{}})
entries = append(entries, fuse.Entry{Name: "AuthSvc", State: false, Instance: &auth.AuthSvc{}})
entries = append(entries, fuse.Entry{Name: "CacheSvc", State: false, Instance: &cache.CacheSvc{}})
entries = append(entries, fuse.Entry{Name: "DBSvc", State: false, Instance: &db.DBSvc{}})
entries = append(entries, fuse.Entry{Name: "OrderSvc", State: true, Instance: &ord.OrderSvc{}})
return entries
}
main(): Uses 'Entries()' and calls 'cfg.Fuse()'. 'cfg' registers and
fuses.
entries := Entries()
errors := cfg.Fuse(entries)
Following code explains it step by step:
package cfg
import (
"github.com/rvauradkar1/fuse/fuse"
)
// Find is used by application packages as a Resource Locator
var Find func(name string) interface{}
// Fuse is used by application main package to provide a list of compoenets to register and fuse
func Fuse(entries []fuse.Entry) []error {
// Step 1. Instance of Fuse
f := fuse.New()
// Step 2. 'cfg.Find' now points to the 'fuse.Find'
Find = f.Find
// Step 3. Register entries
errors := f.Register(entries)
if len(errors) != 0 {
return errors
}
// Step 4. Wire dependencies
return f.Wire()
}
Code snippet from above provides this functionality:
// Find is used by application packages as a Resource Locator
var Find func(name string) interface{}
// 'cfg.Find' now points to the 'fuse.Find'
Find = f.Find
API for mock:
type Mock interface {
// Register a slice of components
Register(entries []fuse.Entry) []error
// Generates mocks
Generate() []error
}
The graph below shows how the mock code generation is laid out.
- main_test.go - Test file for the application main. It depends on the
'mock' package. It uses the 'Entries' method of 'main.go' to register
the components. - main.go - The application main file that supplies the component
entries. - mock package - depends on the 'fuse' package.
All mocks are generated based on a fixed pattern and available in a Go file named mocks_test.go.
'AuthSvc' is a registered component. Clients of this package need to
mock this dependency during unit-testing.
package auth
import (
"fmt"
"time"
)
type IService interface {
Auth(user string) error
}
type AuthSvc struct {
t time.Duration
}
func (a *AuthSvc) Auth(user string) error {
fmt.Printf("Auth for user [%s]\n", user)
return nil
}
package auth
import (
"time"
)
// Begin of mock for AuthSvc and its methods
type MockAuthSvc struct {
t time.Duration
}
type Auth func(s1 string) error
var MockAuthSvc_Auth Auth
func (p *MockAuthSvc) Auth(s1 string) error {
capture("MockAuthSvc_Auth", []interface{}{s1})
return MockAuthSvc_Auth(s1)
}
// End of mock for AuthSvc and its methods
// Original
type AuthSvc struct {
t time.Duration
}
// Mock of Original
type MockAuthSvc struct {
t time.Duration
}
// Method from interface
type IService interface {
Auth(user string) error
}
// Method mock
type Auth func(s1 string) error
// Client provide a mock implementation
var MockAuthSvc_Auth Auth
func (p *MockAuthSvc) Auth(s1 string) error {
// Mocked method is used instead of real code
return MockAuthSvc_Auth(s1)
}
Steps to follow:
- Create a main_test.go file with a Test_Generate method
- Instantiate "mock"
- Register Component Entries
- Generate Mocks
package main
func Test_Generate(t *testing.T) {
fmt.Println("Start mock generation....")
.......
fmt.Println("End mock generation....")
}
func Test_Generate(t *testing.T) {
fmt.Println("Start mock generation....")
// Instantiaze mock, "main" is the name of the directory where your application resides
m := mock.New("main")
......
fmt.Println("End mock generation....")
}
func Test_Generate(t *testing.T) {
fmt.Println("Start mock generation....")
// Instantiaze mock, "main" is the name of the directory where your application resides
m := mock.New("main")
// Reuse the 'Entries' method from 'main.go'
entries := Entries()
// Register the entries, mocks are generated based upon the component structures
errors := m.Register(entries)
......
fmt.Println("End mock generation....")
}
func Test_Generate(t *testing.T) {
fmt.Println("Start mock generation....")
// Instantiaze mock, "main" is the name of the directory where your application resides
m := mock.New("main")
// Reuse the 'Entries' method from 'main.go'
entries := Entries()
// Register the entries, mocks are generated based upon the component structures
errors := m.Register(entries)
if len(errors) != 0 {
log.Fatal(errors)
}
// Generate the mocks
errors = m.Generate()
if len(errors) != 0 {
log.Fatal(errors)
}
fmt.Println("End mock generation....")
}
Following code creates a component instance and intializes the mocked dependencies.
func Test_SaveOrder(t *testing.T) {
// Make instance and initialize mocked dependencies.
svc := OrderSvc{DBSvc: &MockDBSvc{}, CacheSvc: &MockCacheSvc{}}
.......
}
func Test_SaveOrder(t *testing.T) {
// Make instance and initialize mocked dependencies.
svc := OrderSvc{DBSvc: &MockDBSvc{}, CacheSvc: &MockCacheSvc{}}
// Mock dependent methods.
MockDBSvc_AddOrder = func(order string) error { return nil }
MockCacheSvc_AddOrd = func(cart string, status string) error { return nil }
.........
}
Call svc.SaveOrder
func Test_SaveOrder(t *testing.T) {
// Make instance and initialize mocked dependencies.
svc := OrderSvc{DBSvc: &MockDBSvc{}, CacheSvc: &MockCacheSvc{}}
// Mock dependent methods.
MockDBSvc_AddOrder = func(order string) error { return nil }
MockCacheSvc_AddOrd = func(cart string, status string) error { return nil }
// Execute logic
err := svc.SaveOrder("new order", "stats")
// Error handling omitted
fmt.Println(err)
........
}
The generated code provided a method called 'Calls' with signature:
type Params []interface{}
func Calls(name string) []Params
Tests call this method to ensure that dependent method was called. A slice of parameters is returned.
These are the parameters passed to the method. Multiple called to dependent
method result in slice length > 1.
func Test_SaveOrder(t *testing.T) {
// Make instance and initialize mocked dependencies.
svc := OrderSvc{DBSvc: &MockDBSvc{}, CacheSvc: &MockCacheSvc{}}
// Mock dependent methods.
MockDBSvc_AddOrder = func(order string) error { return nil }
MockCacheSvc_AddOrd = func(cart string, status string) error { return nil }
// Execute logic
err := svc.SaveOrder("new order", "stats")
// Error handling omitted
fmt.Println(err)
// Ensure call to MockDBSvc_AddOrder was made
fmt.Println("Number of calls to MockDBSvc_AddOrder = ", NumCalls("MockDBSvc_AddOrder"))
// Print params passed to MockDBSvc_AddOrder
for i, c := range CallParams("MockDBSvc_AddOrder") {
fmt.Println(" Call Number ", i, " Params` = ", c)
}
// Ensure call to MockCacheSvc_AddOrd was made
fmt.Println("Number of calls to MockCacheSvc_AddOrd = ", NumCalls("MockCacheSvc_AddOrd"))
// Print params passed to MockCacheSvc_AddOrd
for i, c := range CallParams("MockCacheSvc_AddOrd") {
fmt.Println(" Call Number ", i, " Params` = ", c)
}
}