A few approaches to test go language code

Ruby and JavaScript were my primary languages until I got my eye on Go. Ruby and JavaScript are incredible languages. Ruby has one of the most advanced testing frameworks called Rspec. JavaScript is a full-stack engineers dream language. JavaScripts server side framework called NodeJS boasts of NPM which is one of the largest package repositories in the world (at the time of writing).

Go is different. It’s the first language to have been developed in the age of the modern computer with features like in-built concurrency.

TDD in Go is very different. If you start writing your Go code without tests, it’s more than likely that you will have to rewrite it when you start adding tests.

While testing, we should be able to mock calls to functions being used in the function we are testing. In other words, we should be able to mock our dependencies.

I have explored 3 ways of achieving that.

  1. “Using global variables” and assigning them to dependencies.
  2. “Providing dependency as a parameter” to the function you are testing.
  3. “Adding the dependency as a field” in your struct (depends if your function is attached to a struct).

For the purpose of explaining the three approaches this is the basic code we want to test:
Note: We will change this code depending on the approach we take.

dependency.go contains:

package dependency
import "fmt"
func HelloWorld() error {
    fmt.Println("Hello world")
return nil
}

myCode.go

package mycode
import "dependency"
type myStruct struct{}
func (m *myStruct) CallHelloWorld() error {
    err := dependency.HelloWorld()
    return err     
}

First Approach: Using global variables

In this approach, we use a function expression for HelloWorld in dependency.go

dependency.go would now become

package dependency
import "fmt"
var HelloWorld = func() error {
    fmt.Println("Hello World")
return nil
}

The test for myCode.go would look like:

myCode_test.go

package mycode
import (
    "fmt"
    "dependency"
)
func TestCallHelloWorld(t *testing.T) {
    // Store the original HelloWorld in a variable
// before starting the test
var originalHelloWorld = dependency.HelloWorld
    // Make sure to assign the correct function to 
// dependency.HelloWorld before exiting
defer func() { dependency.HelloWorld = originalHelloWorld }()
    dependency.HelloWorld = func() error {
        fmt.Println("This is the overridden HelloWorld func")
return nil
    }
    err := CallHelloWorld()

if err != nil {

t.Errorf("expected error to be: nil, but got: %v", err)
    }
}
func TestCallHelloWorldErr(t *testing.T) {
// Store the original HelloWorld in a variable
    var expErr = fmt.Errorf("there was an error")
// before starting the test
var originalHelloWorld = dependency.HelloWorld
// Make sure to assign the correct function to
// dependency.HelloWorld before exiting
defer func() { dependency.HelloWorld = originalHelloWorld }()

dependency.HelloWorld = func() error {
        fmt.Println("This is the overridden HelloWorld func")
return expErr
    }
    err := CallHelloWorld()

if err.Error() != expErr.Error() {
        t.Errorf("expected error to be: %v, but got: %v", expErr, err)
    }
}

Second Approach: Providing dependency as a parameter

In this approach we send the function to call as a parameter to CallHelloWorld

CallHelloWorld would become like this:

myCode.go

package mycode
// Dependency is removed as now the func calling CallHelloWorld will // import it
// import "dependency"
type myStruct struct{}
func (m *myStruct) CallHelloWorld(helloWorld func() error) error {
    err := helloWorld()
    return err
}

myCode_test.go becomes

package mycode
import (
"fmt"
"dependency"
)
var expErr = fmt.Errorf("there was an error")
// Create your own HelloWorld
func myHelloWorld() error {

return nil
}
func myHelloWorldErr() error {

return expErr
}
func TestCallHelloWorld(t *testing.T) {
    err := CallHelloWorld(myHelloWorld)

if err != nil {

t.Errorf("expected error to be: nil, but got: %v", err)
    }
}
func TestCallHelloWorldErr(t *testing.T) {
    err := CallHelloWorld(myHelloWorldErr)

if err.Error() != expErr.Error() {
        t.Errorf("expected error to be: %v, but got: %v", expErr, err)
    }
}

Third Approach: Adding the dependency as a field

In this approach our dependency has an interface. Our struct has a field of type our dependency interface. In our tests we implement the interface and hence are able to mock it.

myCode.go looks like

package mycode
import "dependency"
type myStruct struct{

dependencies depenedency.DependencyInterface
}
func (m *myStruct) CallHelloWorld(helloWorld func() error) error {
    err := m.dependencies.HelloWorld()
    return err
}

dependency.go becomes:

package dependency
import "fmt"
type DependencyInterface interface{
    HelloWorld() error
}
type DependencyStruct struct{}
func (d *DependencyStruct) HelloWorld() error {
    fmt.Println("Hello world")
return nil
}

Finally our test file myCode_test.go becomes

package mycode
import (
"fmt"
"testing"
)
var expErr = fmt.Errorf("there was an error")
type MockDependencyStruct struct {
Err error
}
func (m *MockDependencyStruct) HelloWorld() error {
return m.Err
}
func TestCallHelloWorld(t *testing.T) {
    initializeStruct := &myStruct{
dependencies: &MockDependencyStruct{
Err: nil,
},
}

err := initializeStruct.CallHelloWorld()

if err != nil {

t.Errorf("expected error to be: nil, but got: %v", err)

}
}
func TestCallHelloWorldErr(t *testing.T) {
    initializeStruct := &myStruct{
dependencies: &MockDependencyStruct{
Err: expErr,
},
}

err := initializeStruct.CallHelloWorld()

if err.Error() != expErr.Error() {

t.Errorf("expected error to be: %v, but got: %v", expErr, err)

}
}

Out of the three approaches, approach 3 is a little more verbose but it is also very powerful. The 1st approach seems more like a hack.

I really like the 3rd approach and that’s how I design my current code. It makes my tests very simple and quick to write.

In my next blog, I will write about how I structure my test and how I structure my dependency mocks.

Please feel free to comment about what you think of these approaches.


Quote from 1 of my mentors (Todd Mcleod)

The HIGHER I GO
THE HARDER IT BECOMES TO ADMIT I DO NOT KNOW
THE MORE I ADMIT I DO NOT KNOW
THE HIGHER I GO

Resource:

Example of the above approach on github: https://github.com/nirvanagit/GolangTestingApproaches

Excellent Golang Tutorial: https://www.udemy.com/learn-how-to-code/