
Shim
The most user friendly shimming library for go!
Use this library to overwrite a function from an imported package (that isn't available via an interface) with your own stub for testing.
Look & Feel
Let's assume we have a function something()
that calls os.Create()
under the hood. But for unit testing we don't actually want to create a file every time
or we want to test the behavior in case of an error on create. Let's just replace the original with a stub where we control what it returns.
First, remove the dot .
for imported functions you want to mock and declare them as a variable:
something.go
:
import "os"
+var(
+ osCreate = os.Create
+)
func something() {
- file, err := os.Create("./foo")
+ file, err := osCreate("./foo")
}
Then, replace osCreate
with something you control in your unit test. For instance return a test error.
something_test.go
:
func TestSomething(t *testing.T) {
shim.Run(
func() {
err := something()
assert.Error(t, err)
},
shim.Replace(&osCreate).
With(func(name string) (*os.File, error) {
return nil, fmt.Errorf("test error")
}),
)
}
Inject receiver functions
It also works for receiver functions on structs of 3rd party libraries that don't offer interfaces, we just have to instantiate the object and the function as a
var:
something.go
:
import "os"
+var(
+ file *os.File
+ fileRead = file.Read
+)
func something() {
file, err := osCreate("./foo")
- _, err = file.Read([]byte{})
+ _, err = fileRead([]byte{})
}
The injection works similar to described above, just add another replacement into the shim.Run()
function:
shim.Run(
...
shim.Replace(&fileRead).
With(func(b []byte) (n int, err error) {
return 0, fmt.Errorf("test error")
}),
)
For integrated examples check out examples/main.go
and examples/main_test.go
.
Why another library?
This library essentially offers a nice API around this manual shimming pattern:
func TestSomething(t *testing.T) {
osCreateOrig := osCreate
osCreate = func(name string) (*os.File, error) {
return nil, fmt.Errorf("test error")
}
err := something()
assert.Error(t, err)
osCreate = osCreateOrig
}
The issue with this is that you always have to store the original function and restore it at the end. Otherwise it can have unintentional side effects on other
test executions.
The API is also fully typed using go generics. Whatever type you put in Replace()
you have to put into With()
as well, as the type gets inferred by the
first call. Your IDE will give you those type hints as code completion.
An alternative to this approach is to create an interface where the production implementation actually calls the underlying function and a test implementation
mocks the response. However, that's more verbose for types that don't already offer an interface in the 3rd party library and also with this approach the
production implementation will leave over some untested lines of code.
For library functions that already offer interfaces I don't recommend this library, but rather gomock.