Mocking functions in Go
Apr 10, 2014Functions in Go are first class citizens, that means you can have a variable that contains a function value, and call it like a regular function.
printf := fmt.PrintfThis ability can come in very handy for testing code that calls a function which is hard to properly test while testing the surrounding code. In Juju, we occasionally use function variables to allow us to stub out a difficult function during tests, in order to more easily test the code that calls it. Here’s a simplified example:
printf(“This will output %d line.\n”, 1)
// in install/mongodb.go
package install
func SetupMongodb(path string) error {
// suppose the code in this method modifies files in root
// directories, mucks with the environment, etc…
// Actions you actively don’t want to do during most tests.
}
So, suppose you want to write a test for Bootstrap, but you know SetupMongodb won’t work, because the tests don’t run with root privileges (and you don’t want to setup mongodb on the dev’s machine anyway). What can you do? This is where mocking comes in.
// in startup/bootstrap.go
package startup
func Bootstrap() error {
…
path := getPath()
if err := install.SetupMongodb(path); err != nil {
return err
}
…
}
We just make a little tweak to Bootstrap:
package startupNow if we want to test Bootstrap, we can mock out the setupMongo function thusly:
var setupMongo = install.SetupMongodb
func Bootstrap() error {
…
path := getRootDirPath()
if err := setupMongo(path); err != nil {
return err
}
…
}
// in startup/bootstrap_test.go
package startup
type fakeSetup struct {
path string
err error
}
func (f *fakeSetup) setup(path string) error {
f.path = path
return f.err
}
TestBootstrap(t *testing.T) {
f := &fakeSetup{ err: errors.New(“Failed!”) }
// this mocks out the function that Bootstrap() calls
setupMongo = f.setup
err := Bootstrap()
if err != f.err {
t.Fail(“Error from setupMongo not returned. Expected %v, got %v”, f.err, err)
}
expPath := getPath()
if f.path != expPath {
t.Fail(“Path not correctly passed into setupMongo. Expected %q, got %q”, expPath, f.path)
}
// and then try again with f.err == nil, you get the idea
}
Now we have full control over what happens in the setupMongo function, we can record the parameters that are passed into it, what it returns, and test that Bootstrap is at least using the API of the function correctly.Obviously, we need tests elsewhere for install.SetupMongodb to make sure it does the right thing, but those can be tests internal to the install package, which can use non-exported fields and functions to effectively test the logic that would be impossible from an external package (like the setup package). Using this mocking means that we don’t have to worry about setting up an environment that allows us to test SetupMongodb when we really only want to test Bootstrap. We can just stub out the function and test that Bootstrap does everything correctly, and trust that SetupMongodb works because it’s tested in its own package.