How to mock a package method in Go?

Viewed 686

Let's say I have a package with the following code:

package paths

type FilePath struct {
    PathA string
}

func (c FilePath) GetPathA() string {
    if err := PathExists(PathA); err != nil {
    return ""
    }
    return PathA + "foo"
}

func PathExists(p string) error {
    // call os and file methods
    return err
}

How do I mock out the PathExists dependency to test FilePath? Also, method PathExists is being used by a lot of other packages as well. (I am open to suggestions of refactoring this to make it test friendly, keeping the following pointers in mind)

I have come across a few different approaches but none of them seems intuitive or idiomatic to me.

  1. Have a global variable PE := PathExists in the package; in GetPathA, call err := PE(PathA) and in the test overwrite PE with a mock method.

    Issue: If test package is something like paths_test, I will have to export PE which allows clients of the package to overwrite it as well.

  2. Make PathExists a field of FilePath and mock the field in test.

    Issue: Clients when using the package, will have to initialize PathExists field, or I provide a constructor of the form NewFilePath(PathtA string) which initializes the fields for me. In the actual use case there are a lot of fields, hence this approach fails as well.

  3. Use an interface and embed it within the struct. When client uses it initialize with the actual method and for test mock it.

    type PathExistser interface{ 
        PathExists(p string) error 
    }
    
    type FilePath struct{
        PathA string
        PathExister
    }
    
    type Actual struct{}
    
    func (a Actual) PathExists(p string) error {
       return PathExists(p)
    }
    

    Issue: Client again needs to provide the right implementation of the interface.

I have learnt of few more approaches doing something simimlar to the above options, such as make the method PathExists an argument for GetPathA, etc. All have the same concerns. Basically, I don't want the users of this package to have to figure out what should be the right input parameter to make sure the struct works as expected. Neither do I want the users to overwrite the behaviour PathExists.

This seems like a very straightforward problem and I seem to be missing something very funamental about go testing or mocking. Any help would be appreciated, thanks.

Method names are just for example. In reality GetPathA or PathExists would be way more complex.

1 Answers

To address the issue from your 1. approach, you can use an internal package which you'll then be able to import in paths_test but clients of your package won't be.

package paths

import (
    // ...
    "<your_module_path>/internal/osutil"
)

func PathExists(p string) error {
    return osutil.PathExists(p)
}
package osutil

var PathExists = func(p string) error {
    // call os and file methods
    return err
}

// Use a mutex to make sure that if you have
// multiple tests using mockPathExists and running
// in parallel you avoid the possiblity of a data race.
//
// NOTE that the mutex is only useful if *all* of your tests
// use MockPathExists. If only some do while others don't but
// still directly or indirectly cause the paths.PathExists
// function to be invoked then you still can run into a data
// race problem.
var mu sync.Mutex

func MockPathExists(mock func(p string) error) (unmock func()) {
    mu.Lock()
    original := PathExists
    PathExists = mock
    return func() {
        PathExists = original
        mu.Unlock()
    }
}
package paths_test

import (
    // ...
    "<your_module_path>/internal/osutil"
)

func TestPathExists(t *testing.T) {
    unmock := osutil.MockPathExists(myPathExistsMockImpl)
    defer unmock()
    // do your test
}
Related