Test Go Function That Calls Another Function

A very popular question people ask when working with Go and HTTP clients is how to test the code with go test. You will see many mentions of interfaces, test doubles (stubs, mocks, fakes, etc.), httptest, and so on.

I had a similar problem but it was like this: a function in module A was doing some work on the results returned by a function in module B. In other words, module A was depending on module B. Module B is expected to have its own tests, which must not be repeated in the tests written for module A; why duplicate the effort? How could I test module A without also testing module B in the tests written for module A?

Let's clarify with some example code,

// module: "github.com/example/modulea"
// file: main.go
package main

import (
    "fmt"
    "github.com/example/moduleb"
)

func main() {
    funcA()
}

func funcA() {
    result := moduleb.funcB()
    fmt.Println(result)
}

How could we rewrite the code so it can be tested in both moduleb (someone else's responsibility) and modulea (our responsibility)? Let's see the code before we discuss it in detail.

// module: "github.com/example/moduleb"
// file: b.go
package moduleb

import (
    "fmt"
)

type Data struct {
    foo string
}

type Result string

func (d Data) DoWork() Result {
    return fmt.Sprintf("Did some work in B on data %v", d.foo)
}

// module: "github.com/example/modulea"
// file: main.go
package main

import (
    "fmt"
    "github.com/example/moduleb"
)

type Action interface {
    DoWork() moduleb.Result
}

func main() {
    d := moduleb.Data{
        foo: "PRODUCTION DATA",
    }
    funcA(d)
}

func funcA(act Action) {
    result := act.DoWork()
    fmt.Println(result)
    fmt.Println("Did some more work in A")
}

// module: "github.com/example/modulea"
// file: main_test.go
package main

import (
    "fmt"
    "testing"
    "github.com/example/moduleb"
)

type FakeWork struct {
    moduleb.Data
}

func (fw FakeWork) DoWork() moduleb.Result {
    return fmt.Sprintf("Did some fake work in testing on data %v", fw.foo)
}

func TestMain(t *testing.T) {
    fakew := FakeWork{
        foo: "TEST DATA",
    }
    result := funcA(fakew)
    t.Log(result)
}

What happens when you run the code versus the tests?

$ go run .
Did some work in B on data PRODUCTION DATA
Did some more work in A

$ go test -v .
=== RUN   TestMain
Did some fake work in testing on data TEST DATA
Did some more work in A
--- PASS: TestMain (0.00s)
PASS
ok      github.com/example/modulea  0.185s

You can see the production code path is using production data and test is using the test code path.

Nested (embedded) struct

The main ingredient of this code is nested (embedded) struct.

The base struct is Data{} from moduleb. It has a method DoWork() which does some work and returns results. This method is expected to already be tested by the maintainers of moduleb. The dilemma is that we need to call this method from the production code path and we need to fake it for our tests.

We use the natural properties of structs and embedded structs in Go. When a struct (e.g. L) is embedded in another struct (e.g. H), the fields and methods of L become the fields and methods of H. In production code path we use the method of moduleb, DoWork(), as-is. In the testing code we override the DoWork() method of moduleb with our own, thanks to the Go language properties.

Go allows a struct with an embedded struct to override any method of the embedded struct. In our example above, if we define a method of H with the same signature as the method of L, when we call H.Method(), it calls the version of Method() overridden by H.

We use this behavior to define a fake version of DoWork() in our tests. Now we don't need to run the DoWork() from moduleb which may be doing a lot of work which could be consuming resources like time, memory, network, etc. Instead, we fake that work and respond quickly.

Interface

The second ingredient we use is interface. DoWork() from moduleb is a method of a particular type. DoWork() in main_test.go is a method of another type. If we tried to pass both to funcA(), it wouldn't work. Go's answer to this problem is an interface. Since both types implement a method with the same signature, we can make them related to each other through an interface. Consequently, the receiving function (funcA()) must expect an interface instead of a struct or something else. This is known as the accept interfaces, return structs rule of thumb.

We define an interface, Action, in package main and use it in production code path as well as in tests.

Pass a variable of the type that implements an interface to the function that accepts the interface. In main() above we pass d := Data{} to funcA(), where d is a variable of type Data{} which implements Action interface. In TestMain() we pass fakew := FakeWork{} to funcA(), where fakew is a variable of type FakeWork{} which is not the same as type Data{} but implements the same interface (Action) as Data{}. In both cases since funcA() accepts Action interface, variables of types Data{} and FakeWork{} can be passed to it. funcA() will run the DoWork() method of the type it was passed.

Method

The third ingredient to make this work was using a method instead of a function.

A function can take multiple arguments, one of which could be a special type. A method is another variation where the special type is the receiver argument instead. By switching to methods, we get the benefits of the first two ingredients discussed above.

Use methods more often because they help to write re-usable and fakeable code.

No Mock

Notice there is no mock library in use. We did not need it for this case. Instead, we used fakes.

Mix Multiple Types in the Same Interface?

Yes, we can mix multiple types in the same interface.

package main

import "fmt"

type one struct{}

func (o one) Do() {
    fmt.Println("ONE Do")
}

type two struct{}

func (t two) Make() {
    fmt.Println("TWO Make")
}

type Common interface {
    Do()
    Make()
}

type Fake struct {
    one
    two
}

func (f Fake) Do() {
    fmt.Println("FAKE Do")
}
func (f Fake) Make() {
    fmt.Println("FAKE Make")
}

type Prod struct {
    one
    two
}

func main() {
    f := new(Fake)
    f.Do()
    f.Make()

    o := new(one)
    o.Do()

    t := new(two)
    t.Make()

    tmp(f)

    p := new(Prod)
    tmp(p)

}

func tmp(c Common) {
    fmt.Println("Running func")
    c.Do()
    c.Make()
}

Prod mixes two types in the same struct, each of which has its own method. Methods of both structs are in the same interface.

Fake does the same. Additionally, it overrides the methods of both structs.

Conclusion

Understanding interfaces can be a little confusing for newcomers. It was for me. This example gave me a good understanding of the concept. The key to unlocking the solution to this problem was learning more about the combination of embedded structs and interfaces.

When you build a module that is meant to be re-used, use structs, custom types, and methods instead of simple functions. It will allow users to create their own fakes. In some cases you may also want to ship some recommended fakes in the module.

Further Reading