Simple Go dependency injection (Part 2)
Aug 15, 2018 · 5 minute read · CommentsSimple Go dependency injection (Part 2)
Welcome to part 2 of 2 in this brief series dedicated to Go dependency injection. In Simple Go Dependency Injection Part 1 we saw how to replace 1 method with a variable so that we could at test time ‘inject’ a mock function to replace the real one and therefore return behavior required for the unit test you were coding. That method is simple works well but what if the thing you are trying to mock is a struct with attached methods, it now becomes much more complicated to try to turn all methods into variables and then use those as you can imagine this could quickly turn into a nightmare. Interfaces to the rescue.
Interfaces in go describe a set of behaviors that all relate in someway to one another. As an example, a ‘reader’ which we know reads data and might do so in many ways, readBytes, readString, readInt, readFile, etc,… and now if you had to read from file, from TCP connections, from … each one of the ‘readers’ would implement the same methods so we could create an interface and then instead of passing a concrete implementation like FileReader we would simply inject a ‘reader’ and the user of the reader at runtime would never have to worry about type of reader it was, he would simply call the reader functions and expect the data to come back in a standard way. So for unit test you could inject a “mock” and allow your mock to provide consistent test data to your process under test. Below is a more concrete example. Here we want to use mongo to increment a number and return to me the next number (sequence generator). The interface we need to create is the same interface that mongo uses (Session, Database, Collection, Query).
Here are the interfaces we need:
package testapp
import "github.com/globalsign/mgo"
// Session is an interface to access to the Session struct.
type Session interface {
DB(name string) DataLayer
Close()
}
// DataLayer is an interface to access to the database struct.
type DataLayer interface {
C(name string) Collection
}
// Collection is an interface to access to the collection struct.
type Collection interface {
Find(query interface{}) Query
}
// Query is an interface to access to the Query struct.
type Query interface {
Apply(change mgo.Change, result interface{}) (info *mgo.ChangeInfo, err error)
}
Once we have interface we need to create 2 separate implementation of those interface: a concrete implementation that talks to mongo for real and a mock that simulates data for unit tests.
The concrete implementation could look something like this:
package clients
import (
"github.com/globalsign/mgo"
"testapp"
)
// NewSession returns a new Mongo Session.
func NewSession() testapp.Session {
mgoSession, err := mgo.Dial("localhost:27017")
if err != nil {
panic(err)
}
return MongoSession{mgoSession}
}
// MongoSession is currently a Mongo session.
type MongoSession struct {
*mgo.Session
}
// DB shadows *mgo.DB to returns a DataLayer interface instead of *mgo.Database.
func (s MongoSession) DB(name string) testapp.DataLayer {
return &MongoDatabase{Database: s.Session.DB(name)}
}
// MongoCollection wraps a mgo.Collection to embed methods in models.
type MongoCollection struct {
*mgo.Collection
}
func (m MongoCollection) Find(query interface{}) testapp.Query {
return &MongoQuery{
Query: m.Collection.Find(query),
}
}
// MongoDatabase wraps a mgo.Database to embed methods in models.
type MongoDatabase struct {
*mgo.Database
}
// C shadows *mgo.DB to returns a DataLayer interface instead of *mgo.Database.
func (d MongoDatabase) C(name string) testapp.Collection {
return MongoCollection{Collection: d.Database.C(name)}
}
type MongoQuery struct {
*mgo.Query
}
func (q MongoQuery) Apply(change mgo.Change, result interface{}) (info *mgo.ChangeInfo, err error) {
return q.Query.Apply(change,result)
}
And the mock like this:
package mocks
import (
"testapp"
"github.com/globalsign/mgo"
"github.com/globalsign/mgo/bson"
)
// MockSession satisfies Session and act as a mock of *mgo.session.
type MockSession struct{}
// NewMockSession mock NewSession.
func NewMockSession() testapp.Session {
return MockSession{}
}
// Close mocks mgo.Session.Close().
func (fs MockSession) Close() {}
// DB mocks mgo.Session.DB().
func (fs MockSession) DB(name string) testapp.DataLayer {
mockDatabase := MockDatabase{}
return mockDatabase
}
// MockDatabase satisfies DataLayer and act as a mock.
type MockDatabase struct{}
// MockCollection satisfies Collection and act as a mock.
type MockCollection struct{}
// Find mock.
func (fc MockCollection) Find(query interface{}) testapp.Query {
return MockQuery{}
}
// C mocks mgo.Database(name).Collection(name).
func (db MockDatabase) C(name string) testapp.Collection {
mockCollection := MockCollection{}
return mockCollection
}
type MockQuery struct{}
var ApplyResult = map[string]interface{} {
"siteId": "fp-us",
"NextId": 9999,
"prefix": "FX",
}
func (mq MockQuery) Apply(change mgo.Change, result interface{}) (info *mgo.ChangeInfo, err error) {
*result.(*bson.M) = ApplyResult
return &mgo.ChangeInfo{}, nil
}
Once that is done let’s create the code that we will use to test:
package main
import (
"testapp"
"fmt"
"testapp/clients"
"github.com/pkg/errors"
"github.com/globalsign/mgo"
"github.com/globalsign/mgo/bson"
)
var (
session testapp.Session
collection testapp.Collection
)
func init() {
session = clients.NewSession()
collection = session.DB("CheckoutService").C("order_numbers")
}
func GetNextNumber() (int, string, error) {
doc := bson.M{}
change := mgo.Change{
Update: bson.M{"$inc": bson.M{"NextId": 1}},
ReturnNew: true,
}
query := collection.Find(bson.M{"siteId": "fp-us"})
_, err := query.Apply(change, &doc)
if err != nil {
return -1, "", errors.New("Could not get an ID from Mongo")
}
return doc["NextId"].(int), doc["prefix"].(string), nil
}
func main() {
num, prefix, err := GetNextNumber()
if err != nil {
panic(err)
}
fmt.Printf("%s%08d\n", prefix, num)
}
Above you can see that in the init a new mongo Sessions is created and the main function we call GetNextNumber to retrieve the next number in the sequence from mongo. To unit test this we will need to create a mock session and then call GetNextNumber which will use that mock session to access then DB, a Collection and a Query, see the example below.
Finally here are the unit tests:
package main
import (
"testing"
"testapp/mocks"
)
func Test_GetNextNumber(t *testing.T) {
session = mocks.NewMockSession()
collection = session.DB("CheckoutService").C("order_numbers")
num, prefix, err := GetNextNumber()
if prefix != "FX" {
t.Errorf("Error: %s\n", err.Error())
}
if num != 9999 {
t.Errorf("Error: %d", num)
}
}
I hope this short and hopefully complete example helps you figure out how to mock your dependancies in the future.
Happy Going!!!