I'm trying to figure out how to write tests for a simple function that calls a function. Now I'm aware that this has been asked before, but none of the answers I've seen so far have shown how one might write it such that the calling code doesn't need deep knowledge of the code it's calling to run.
For a dummy example, say I've got a file called finder.go
:
package finder
import (
"errors"
"os/exec"
)
// Writing things this way, finder.Get() just "knows" how to answer the
// question of which program, if any is installed.
func Get() (string, error) {
pth, err := exec.LookPath("thing1")
if err == nil {
return pth, nil
}
if !errors.Is(err, exec.ErrNotFound) {
return "", err
}
pth, err = exec.LookPath("thing2")
if err == nil {
return pth, nil
}
return "", errors.New("you must have either thing1 or thing2 installed for this to work")
}
and a file in another package that just calls that function:
package main
import (
"fmt"
"os"
"path/to/whatever/finder"
)
// The calling function doesn't need any knowledge of what finder.Get() is
// doing or what it might need, it just says "gimme program if you have it".
func main() {
path, err := finder.Get()
if err != nil {
fmt.Errorf("it's broken")
os.Exit(1)
}
fmt.Println(path)
}
The only advice I've found so far to make this testable is to change the program to require that main()
"know" that finder.Get()
needs some sort of "pathfinder" function to do its job, so main()
needs to pass that function to it. Alternatively, you can define a struct with a couple methods and pass a function to the struct when creating it: it's the same solution with extra steps.
What I want to know is if it's even possible to write a go program where the calling function doesn't need to know anything about the function it's calling other than what it's expected to return. I mean, I have that now above and it works, but I want to write tests for this. If it's impossible in this language, I'd really like to know how people manage to write complex software with it if they can't abstract complexity away like this and still have tests. Maybe there's a pattern that I'm not familiar with?
In Python, this is pretty straightforward: you just mock out exec.LookPath
:
@patch("path.to.finder.exec.LookPath")
def test_get():
...
...but as I understand it, monkeypatching isn't a thing in Go, so what other options are there? Surely not all Go programs require callers to pass all the tools needed to do a job to the function they're calling all the time... right?