DEV Community

Cover image for Go tests - How to verify if something was called?
Gustavo Lopes
Gustavo Lopes

Posted on

Go tests - How to verify if something was called?

Hello, gophers

I was talking to a friend and remembering some challenges we faced when we start our journey in Golang. For context, we both came from a NodeJS background, a land where there are some tests frameworks like the famous Jest which provide methods like .toHaveBeenCalled(), toHaveBeenCalledTimes(number) and .toHaveBeenCalledWith(...), etc.

On other hand, at the Go world, the native testing package suits well for (almost) all scenarios of unit tests, but it does not provide a clear way to do things like verifying if something was called and the plot twist is that it doesn't need to. As Go provide great idiomatic ways to perform thing like this, we don't need to use any framework.

Okay, but how to verify something was called in "Go idiomatic way"?

The answer is simple: using interfaces and dependency inversion!

So, let's talk a bit about dependency inversion

As a good practice of encapsulation, it's recommended that we use interfaces to abstract types whenever is possible in our code. For example, suppose we want to define structs that represent dogs of specific breeds, like these:

package dogs

type DogLhasa struct {
    Name string
} 

func (d *DogLhasa) Bark() string {
    return "woof-AUAU"
}

type DogRotweiler struct {
    Name string
}

func (d *DogRotweiler) Bark() string {
    return "woof-woof-ARHHHHHGG"
}
Enter fullscreen mode Exit fullscreen mode

And suppose that we have a function that calls the Bark() method of a dog passed as an argument. The question is how to define this function? Without using interfaces, we have to do something weird like this:

func MakeDogLhasaBark(dog DogLhasa) string {
    return dog.Bark()
}

func MakeDogRotweilerBark(dog DogRotweiler) string {
    return dog.Bark()
}
Enter fullscreen mode Exit fullscreen mode

It's clear that this doesn't scale well, right? Imagine what a mess it would be if we have to write a function like this for every dog bread we want to add to our code.
To work around this, we must work with interfaces. We define an interface that defines what a dog should be, and every code that deals with dogs can relay up this interface.

type Dog interface {
    Bark() string
}

func MakeDogBark(dog Dog) string {
    return dog.Bark()
}
Enter fullscreen mode Exit fullscreen mode

And now when we call MakeDogBark, we can pass whatever object of the type of some struct that implements the interface Dog.

func main() {
    dog := &DogLhasa{Name: "Rex"}
    MakeDogBark(dog)
}
Enter fullscreen mode Exit fullscreen mode

A lot simpler, isn't it? This is called dependency inversion - instead of depending on some specific dog struct, our function MakeDogBark depends on the interface Dog that defines what a struct must implement to be a Dog. Thus, the MakeDogBark function can accept any object of any struct that implements the interface Dog. Check this visual representation of dependency inversion:

Dependency inversion representation

But how exactly this can help us to implement tests like the toHaveBeenCalled.* ones?

Let's write a unit test to the function MakeDogBark. In this test, we want to ensure that when MakeDogBark is called, the Bark method of dog passed as argument is called. To do this, we can create a "fake dog" struct that allows us to keep track of how many times the method Bark was called. Like this:

type fakeDog struct {
    numberOfBarks int
}

func (d *fakeDog) Bark() string {
    d.numberOfBarks++
    return "woof"
}
Enter fullscreen mode Exit fullscreen mode

This way, we increase the variable numberOfBarks every time an object of type fakeDog barks. So, to test if the Bark method was called, we just have to check if the numberOfBarks is greater than zero.

func TestMakeDogBark(t *testing.T) {
    dog := &fakeDog{}

    MakeDogBark(dog)

    // fail if the dog didn't bark
    if dog.numberOfBarks == 0 {
        t.Errorf("Expected dog to bark once, but it barked %d times", dog.numberOfBarks)
    }
}
Enter fullscreen mode Exit fullscreen mode

Similar heuristics can be used to achieve variations of toHaveBenCalled.* tests. For example:

toHaveBeenCalledWith

type fakeStruct struct {
    calledWith string
}

func (f *fakeStruct) SomeMethod(s string) {
    f.calledWith = s
}

func TestSomeMethod(t *testing.T) {
    f := &fakeStruct{}
    f.SomeMethod("hello")
    if f.calledWith != "hello" {
        t.Errorf("Expected SomeMethod to be called with 'hello', but was called with '%s'", f.calledWith)
    }
}
Enter fullscreen mode Exit fullscreen mode

toHaveBeenCalledTimes

type fakeStruct struct {
    calledTimes int
}

func (f *fakeStruct) SomeMethod() {
    f.calledTimes++
}

func TestSomeMethod(t *testing.T) {
    f := &fakeStruct{}

    f.SomeMethod()
    f.SomeMethod()
    f.SomeMethod()

    if f.calledTimes != 3 {
        t.Errorf("Expected f.calledTimes to be 3, but it was %d", f.calledTimes)
    }
}
Enter fullscreen mode Exit fullscreen mode

Pretty nice, right? It's amazing how much we can achieve with Golang just using native stuff, with no third-party framework or libraries.

Happy testing! 🧪

Top comments (3)

Collapse
 
marcello_h profile image
Marcelloh

I still can see how we now can know if the Lhasa Apso has barked or not and if so, how many times it did that.
(aka: if we did call the tests for that.)

I can imagine to have extra fields in the struct for measuring the access to it, but at what costs?
It's probably better to look at the code-coverage to see if it is "touched".

Collapse
 
gustavolopess profile image
Gustavo Lopes

actually the test of text is designed to component which has type Dog as dependency. Lhasa Apso is a “concrete” implementation of Dog, therefore you don’t need to mock a Dog to test its methods, you can test them directly :)

Collapse
 
marcello_h profile image
Marcelloh

I know you can test directly, but still I won't have any information about how many times a Lhasa was barking with your example. You will only know that from the fake-dog, and that's the one we're the least interested in, right?