49

I have the following method to test, which uses a function imported from a package.

import x.y.z

func abc() {
    ...
    v := z.SomeFunc()
    ... 
}

Is it possible to mock SomeFunc() in Go?

Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189
Pradeep
  • 1,198
  • 3
  • 12
  • 22

5 Answers5

83

Yes, with a simple refactoring. Create a zSomeFunc variable of function type, initialized with z.SomeFunc, and have your package call that instead of z.SomeFunc():

var zSomeFunc = z.SomeFunc

func abc() {
    // ...
    v := zSomeFunc()
    // ...
}

In tests you may assign another function to zSomeFunc, one that is defined in tests, and does whatever the test wants it to.

For example:

func TestAbc(t *testing.T) {
    // Save current function and restore at the end:
    old := zSomeFunc
    defer func() { zSomeFunc = old }()

    zSomeFunc = func() int {
        // This will be called, do whatever you want to,
        // return whatever you want to
        return 1
    }

    // Call the tested function
    abc()

    // Check expected behavior
}

See related / possible duplicate: Testing os.Exit scenarios in Go with coverage information (coveralls.io/Goveralls)

Community
  • 1
  • 1
icza
  • 389,944
  • 63
  • 907
  • 827
  • How to mock if the function returns more than one value? – user4002112 May 14 '20 at 03:01
  • @user4002112 The same way. Obviously the function used for mocking has to return multiple values too. – icza May 14 '20 at 08:27
  • Doesn't it have race condition problem when we run tests concurrently? – Taeyeong Jeong Mar 15 '21 at 06:00
  • 8
    This is essentially global state. May work for something trivial, but may just as probably cause serious pain to someone who inherits such code (i.e several hours lost debugging). This is not a good pattern in a professional setting. – Ярослав Рахматуллин Nov 06 '21 at 10:37
  • @ЯрославРахматуллин I would agree if the variable's value would be changed during the lifetime of the app. But it is not, its sole purpose is to aid testing, and its value is only changed in tests, so this use does not fall under the global state category. – icza Nov 06 '21 at 12:24
  • 2
    I had to debug a case like that last week. The test failed when run with all other tests, but passed when run in isolation. Global state means any variable or symbol that's globally available and can change (it was changed by other tests), including func-vars. It's literally the definition of global state. – Ярослав Рахматуллин Nov 07 '21 at 00:15
12

One thing you can do is this:

import "x/y/z"

var someFunc = z.SomeFunc

func abc() {
    ...
    v := someFunc()
    ... 
}

And in your test file you would do this.

func Test_abc() {
    someFunc = mockFunc
    abc()
}

But make sure that you do this in a concurrent manner, if you have multiple TestXxx functions calling abc or setting someFunc you may be better of using a struct with a someFunc field.

mkopriva
  • 35,176
  • 4
  • 57
  • 71
  • 1
    Worth noting, you can use the argument `-p 1` to `go test` to force all tests to run in series rather than parallel. – Kaedys Mar 22 '17 at 16:27
  • 1
    This is essentially global state. May work for something trivial, but may just as probably cause serious pain to someone who inherits such code (i.e several hours lost debugging). This is not a good pattern in a professional setting. – Ярослав Рахматуллин Nov 06 '21 at 10:34
  • @Kaedys For this answer the default flag `go test -parallel 1` does make sense. The `-p 1` is however _not_ a synonym for that! The former is goroutine-level, the latter the pid-level. – kubanczyk Feb 08 '22 at 18:12
5

Having function pointer and monkey patching it is the one of doing it. But then when you multiple functions to mock, you will have a number function pointers and unnecessarily you will have to call function using the function pointer only.

The better and the recommended idea to have an interface and make your function part of the struct implementing the interface. Once done that, you can generate mocks using some nice tools available for go.

I have been using this:

mockgen -source myModule.go -package myPackage -destination myModuleMock.go

You can install it by:

go get github.com/golang/mock
Tats_innit
  • 33,991
  • 10
  • 71
  • 77
  • gmock is excellent but does not have a native solution for global functions. you have to wrap them with an interface. – Tareq Sha Sep 13 '19 at 15:37
  • This article https://dev.to/jonfriesen/mocking-dependencies-in-go-1h4d explains the steps with examples for this approach. – inquisitive Jan 23 '20 at 09:07
2

While creating a package level variable is a viable option, it comes with some restrictions. to name a few:

  1. It discourages running parallel tests using t.Parallel() as there can be race conditions on different behaviors of mocked function.
  2. it is dangerous as a future developer in the same package can accidentally update the implementation of this global variable.

another way is to pass along the methods you want to mock as arguments to the function to enable testability. In my case, I already had numerous clients calling this method and thus, I wanted to avoid violating the existing contracts. so, I ended up creating a wrapped function.

eg:

import (
 z "x.y.z"
)

//this should replicate the type of function z from x.y.z
type operation func() resp

func wrappedAbc(op operation) {
  ....
  resp := op()
  ....
}

func Abc() {
  wrappedAbc(z)
}

now for testing the actual logic, you will test calls to wrappedAbc instead of abc and you would pass it a mocked operation. this will allow you to test all the business logic while not violating API contract with current clients of method Abc.

AppleCiderGuy
  • 1,249
  • 1
  • 9
  • 16
1

mockcompose uses an approach that allows you to generate a mocking class, you can direct mockcompose to include your selected dependency closure (any imported functions from other packages). In the mean time, it generates a cloned copy of your subject function with local overrides so that you can test against. You can embed code generation process with go generate, therefore ensure your cloned copy is always in-sync with your code changes.

Say you have a function functionThatUsesGlobalFunction that imports Sprintf in package fmt.

func functionThatUsesGlobalFunction(
    format string,
    args ...interface{},
) string {
    //
    // skip fansy logic...
    //

    // call out to a global function in fmt package
    return fmt.Sprintf(format, args...)
}

Your goal is to test functionThatUsesGlobalFunction with Sprintf in package fmt being mocked.

To do that, you can configure go generate with mockcompose as following:

mocks.go

//go:generate mockcompose -n mockFmt -p fmt -mock Sprintf
//go:generate mockcompose -n mockJson -p encoding/json -mock Marshal
//go:generate mockcompose -n clonedFuncs -real "functionThatUsesGlobalFunction,fmt=fmtMock"
package clonefn

go generate mockcompose will then generate plumbing classes for you, enable you to write test code as following:

package clonefn

import (
    "testing"

    "github.com/stretchr/testify/mock"
    "github.com/stretchr/testify/require"
)

var fmtMock *mockFmt = &mockFmt{}

func TestClonedFuncs(t *testing.T) {
    assert := require.New(t)

    // setup function mocks
    fmtMock.On("Sprintf", mock.Anything, mock.Anything).Return("mocked Sprintf")

    // inside functionThatUsesMultileGlobalFunctions: fmt.Sprintf is mocked
    assert.True(functionThatUsesGlobalFunction_clone("format", "value") == "mocked Sprintf")
}

Please checkout this for details.

tomerpacific
  • 4,704
  • 13
  • 34
  • 52
Kelven Yang
  • 31
  • 1
  • 2
  • This looked promising but I'm not finding it easy to use. Just fighting mockcompose errors all the way, trying to mock [rabbitmq](https://github.com/rabbitmq/amqp091-go) `amqp.Dial`. – Ed Randall Jul 03 '23 at 10:03