0

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?

Daniel Quinn
  • 6,010
  • 6
  • 38
  • 61

2 Answers2

0

I'm not sure what you want to achieve. But Functions are first Class Citizens in go as well. So you could use them as types vor variables or even function parameters as well.

Given a finder Package with the following content

package finder
    
    import (
        "errors"
        "os/exec"
    )
    
    type PathGetter func(string) (string, error)
    
    func DummyGetter(string) (string, error) {
        return "", errors.New("This is a dummmy result")
    }
    
    func RealGetter(Name string) (path string, err error) {
        return exec.LookPath(Name)
    }

Will enable you to use it in a calling package as follows

package main
    
    import (
        "fmt"
        "test/finder"
    )
    
    func DemoGetter(Name string, getter finder.PathGetter) {
        s, err := getter(Name)
        fmt.Printf("Path:%s err:%v\n", s, err)
    }
    
    func main() {
        DemoGetter("DEMO", finder.DummyGetter)
        DemoGetter("DEMO", finder.RealGetter)
    }

The other way of Abstraction would be to use go's interfaces which would be. my favorite.

With interfaces it might look like

package finder

import (
    "errors"
    "os/exec"
)

type Getter interface {
    Get(string) (string, error)
}

type RealGetterI struct{}

func (r RealGetterI) Get(Name string) (string, error) {
    return exec.LookPath(Name)
}

type DummyGetterI struct{}

func (d DummyGetterI) Get(s string) (string, error) {
    return "", errors.New("This is a dummy error")
}

and in the calling packe like

package main

import (
    "fmt"
    "test/finder"
)


func main() {
    var interfacedgetter finder.Getter
    interfacedgetter = finder.DummyGetterI{}
    path, err := interfacedgetter.Get("Demo")
    fmt.Printf("Path:%s err:%v\n", path, err)

    interfacedgetter = finder.RealGetterI{}
    path, err = interfacedgetter.Get("Demo")
    fmt.Printf("Path:%s err:%v\n", path, err)

}
  • Thanks, but this is precisely what I'm trying to avoid. In both of the cases provided here, `main()` must have knowledge of the means that `finder.Get()` uses to do its job. That's not abstraction, but tight coupling. Imagine a more complex process, where finding the right response requires an API call, database lookup and reading from a file. The calling code doesn't need to know this, so there's no reason it should have to pass the instructions on how to do the job to the thing that's doing the job. – Daniel Quinn Jun 11 '23 at 13:19
  • 1
    Ok I understand that point. But that is IMHO the way how Dependency Injection or Inversion of Control works. You need some setup code which configures what should be called. Afterwards the actual caller does not need to know the details. – Reinhard Luediger Jun 11 '23 at 13:27
  • Well then in your example, how does anyone ever test `RealGetter()`? I mean in this case it's just one (never tested) line of code, but in a more realistic case, this function would involve some complexity. If you can't call it without retooling it to accept an argument of its own, then it's just untested interfaces all the way down isn't it? – Daniel Quinn Jun 11 '23 at 13:37
  • Well the Author of the module should be responsible to write the appropriate tests. And of course it is possible to write such tests. In this scenario you would have a tight coupling withe runtime Environment where the tests run. With that you could call the original function with parameters where you know if they exist in the path or not. – Reinhard Luediger Jun 11 '23 at 14:02
0

It is often necessary mock functions that depends on the outside system for testing. A simple trick is to declare the function as a variable alongside the code that needs to be tested:

var LookPath = exec.LookPath

Code that uses LookPath instead of exec.LookPath can now be tested easily by setting this variable from the test. I like this approach because it can be applied without changing the clean function signature and the client code that you have already.

There are of course other options, like those suggested here: Is there an easy way to stub out time.Now() globally during test?

Note that exec.LookPath uses the $PATH environment variable, so it would also work to keep the code exactly as it is, and use os.Setenv to override the path.

Lars Christian Jensen
  • 1,407
  • 1
  • 13
  • 14