Mocking In Go
As mentioned last time around, the testing
package is really barebones and doesn’t provide the BDD concepts that we all know and love like easy mocking and stuff like that.
A few quick notes:
- While I don’t think it should be at all, this entry is about a somewhat divisive topic. In my mind, mock objects are quite possibly the best thing in life for a test-oriented developer. I think the bad rep they get is more or less down to mocking the wrong things at the wrong time. Today, I’m going to focus on the HOW, but you have my word that I’ll talk about the other interrogatives around mocking some day soon.
- Maybe get yourself a beverage. This is going to be relatively heavy on code samples.
Mocking
There are of course mocking packages for Go. From what I’ve seen, a good deal of them involve doing compile-time code generation, introspection, and stuff like that. I’m not a real big fan of that sort of thing, to the point that I don’t really have a suggestion for an off-the-shelf mocking solution. Well, that’s not true. Specifically for mocking HTTP requests against third-party services, packages like httpmock are pretty great, but that’s also relatively niche. If you know me (we’ve already established that you might), you know that I don’t care for one-off niche tools so much.
So, what are we to do? In Ruby projects, I’d use a Plain Ol’ Ruby Object (likely a literal instance of Object
) and attach stubbed methods to it during the test. Go, however, is not nearly as squishy as Ruby. That means the question remains … what are we to do?
We POGO, That’s What
We can implement our very own lightweight mocks with Plain Ol’ Go Objects, POGOs.
Way back in the last post, I mentioned that things are awesome if you design your code with testability in mind. While it’s definitely not the be-all-end-all concept for that idea, a pretty big part of that to me is to use interfaces absolutely anywhere that it makes sense (and in a few cases where it doesn’t). Let’s expand on our already conflated example:
package conflatedexample
type Validator interface {
Validate(interface{}) error
}
Okay, so now we have an interface that describes a validator. Also, we need to update our StringPresence
validator so it implements this interface. Let’s start with the spec:
package conflatedexample
import (
"testing"
)
func TestStringPresence(t *testing.T) {
input := "sausages"
subject := func() *StringPresence {
return &StringPresence{}
}
t.Run("Validate()", func(t *testing.T) {
t.Run("when given a non-string", func(t *testing.T) {
result := subject().Validate(12345)
t.Run("it returns an error regarding the bad data", func(t *testing.T) {
if result == nil {
t.Errorf("expected an error, got nil")
}
})
})
t.Run("when given a string", func(t *testing.T) {
t.Run("that is blank", func(t *testing.T) {
input := ""
result := subject().Validate(input)
t.Run("it returns an error", func(t *testing.T) {
if result == nil {
t.Errorf("expected an error, got nil")
}
})
})
t.Run("that is not blank", func(t *testing.T) {
result := subject().Validate(input)
t.Run("it returns no error", func(t *testing.T) {
if result != nil {
t.Errorf("expected no error, got %s", result.Error())
}
})
})
})
})
}
That looks pretty good. We even added an extra scenario, because the type that we take as an argument in our method is now the any type, so we need to make sure that we handle the possibility that it receives data that it doesn’t know how to handle. Of course, the test fails, because we haven’t updated our code yet. So, let’s do that.
package conflatedexample
import (
"errors"
)
type StringPresence struct{}
func (validator *StringPresence) Validate(candidate interface{}) error {
wrapped, ok := candidate.(string)
if !ok {
return errors.New("not a string")
}
if len(wrapped) == 0 {
return errors.New("cannot be blank")
}
return nil
}
There we go. Now we have a notion of a Validator, and we have a real implementation of that idea that validates that a string contains content. How does that help us with our test?
It Doesn’t
What it does help us test is things that use validators, because now we can mock along the seam that we just created and code to the interface. Let’s throw down a handy mock validator.
package conflatedexample
import (
"errors"
)
var errUnimplemented = errors.New("unimplemented")
// just to save us some confusion, i like having names for things that I reference a lot
type validateImpl func(interface{}) error
// NewMockValidator returns a mocked Validator that is configured to always return a failure
// for all methods. This behavior is configurable by chaining `With...` methods.
func NewMockValidator() *MockValidator {
return &MockValidator{
validate: func(candidate interface{}) error {
return errUnimplemented
},
}
}
type MockValidator struct {
validate validateImpl
}
// Implement the Validator API
func (validator *MockValidator) Validate(candidate interface{}) error {
// to allow for a configurable experience, let's call whatever is stored in the mock
return validator.validate(candidate)
}
// Allow for configurable behavior
func (validator *MockValidator) Clone() *MockValidator {
// to help out with thread safety, let's treat our validator as immutable and create
// a deep copy for modifications
return &MockValidator{
validate: validator.validate,
}
}
func (validator *MockValidator) WithValidate(impl validateImpl) *MockValidator {
// let's make a clone with a specific validatorValidate implementation
tweaked := validator.Clone()
tweaked.validate = impl
return tweaked
}
There we go. Now we have a mock validator that we can use anywhere that we’d need to test code that uses a validator. What’s more, we can configure its behavior by chaining calls to it like so:
myValidator := NewMockValidator().
WithValidate(func(candidate interface{}) error {
// let's return a specific error rather than the default error
return errors.New("this is a very specific error")
})
Use the Mocks
So, right now, our conflated example includes not just a notion of a Validator, but also two Validator implementations. One of those is a real validator that validates string presence, and the other is a mock validator that always says no (unless you tell it to say something else).
Let’s use this to BDD up a reified process that valdates some data before it saves it to a database.
package conflatedexample
import (
"errors"
"testing"
)
// For the purpose of this exercise, let's roll with these
// assumptions:
//
// * There is a struct named Person with a Name field
// * There is an interface named Driver that implements
// database CRUD operations for Person records
//
// To make things even better, I'm going to go ahead and mock the
// Driver in our spec, too.
func TestSavify(t *testing.T) {
t.Run("Save(*Person)", func(t *testing.T) {
validationFailureMessage := "validation failure"
createFailureMessage := "put failure"
errValidation := errors.New(validationFailureMessage)
errCreate := errors.New(createFailureMessage)
badValidate := func(candidate interface{}) error {
return errValidation
}
goodValidate := func(candiate interface{}) error {
return nil
}
badCreate := func(p *Person) error {
return errCreate
}
goodCreate := func(p *Person) error {
return nil
}
validator1 := NewMockValidator().WithValidate(goodValidate)
validator2 := NewMockValidator().WithValidate(goodValidate)
driver := NewMockDriver().WithCreate(goodCreate)
subject := func(driver Driver, validators ...Validator) *Savify {
return NewSavify(driver, validators...)
}
input := &Person{Name: "george"}
t.Run("when any validator fails", func(t *testing.T) {
validator1 := validator1.WithValidate(badValidate)
result := subject(driver, validator1, validator2).Save(input)
t.Run("it returns a validation error", func(t *testing.T) {
if result == nil {
t.Errorf("expected an error, got nil")
}
details := result.Error()
if details != validationFailureMessage {
t.Errorf("expected a validation failure, got %s", details)
}
})
})
t.Run("when all validators succeed", func(t *testing.T) {
t.Run("but the driver can't save the record", func(t *testing.T) {
driver := driver.WithCreate(badCreate)
result := subject(driver, validator1, validator2).Save(input)
t.Run("it returns a create error", func(t *testing.T) {
if result == nil {
t.Errorf("expected an error, got nil")
}
details := result.Error()
if details != createFailureMessage {
t.Errorf("expected a create failure, got %s", details)
}
})
})
t.Run("and the driver saves the record", func(t *testing.T) {
result := subject(driver, validator1, validator2).Save(input)
t.Run("it returns a success", func(t *testing.T) {
if result != nil {
t.Errorf("expected no error, got %s", result.Error())
}
})
})
})
})
}
I know, we sure are writing a lot of code before we … write any code. Let’s look at some things that we know about the thing that we’re going to write based on that spec, though:
Savify
is an object. It has a Driver and a list of Validators as membersSavify
also has a method called Save that takes a Person reference- There’s a friendly
NewSavify()
constructor that takes a driver and a list of validators and gets us aSavify
reference Savify.Save()
cares about whether or not its validations succeed, but totally doesn’t care about what those validations doSavify.Save()
cares about whether or not its driver can create the desired record, but totally doesn’t care about how it does it
Design wise, that’s a pretty good way to be. At the least, it keeps Demeter happy. Let’s go ahead and implement our Savify object:
package conflatedexample
type Savify struct {
driver Driver
validators []Validator
}
func NewSavify(driver Driver, validators ...Validator) *Savify {
return &Savify{
driver: driver,
validators: validators,
}
}
func (s *Savify) Save(person *Person) error {
for _, v := range s.validators {
if err := v.Validate(person.Name); err != nil {
return err
}
}
return s.driver.Create(person)
}
That’s … IT?!
Yup. That’s it … I said it almost a decade ago, and I’ll say it again now: to me, behavior testing is all about expressing intent. It’s almost always the case that the spec for a given chunk of code is going to be larger than said chunk of code.
That’s a great thing in itself, but that’s not the point of this entry. This entry is all about expressing intent in a different way by using our own lightweight mocks instead of reaching out to a third-party package that’s almost certainly full of very clever magic that disallows us from expressing intent.
Now, if you really want to melt your brain on this, consider that the only thing in the project that our tests don’t fully cover is the constructors for our mocks. We could totally write behavior tests for these mocks (and we should). I’m going to leave that as an exercise for you, dear friend. Also, you’ll need to come up with the meat of both the Driver interface as well as the mock Driver implementation. I know you can do it.
Next time around, we’re going to take a moment to talk about the differences between Test-Driven Development and Behavior-Driven Development, and I’ll talk a little about why I prefer BDD.