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"
}
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()
}
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()
}
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)
}
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:
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"
}
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)
}
}
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)
}
}
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)
}
}
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)
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".
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 :)
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?