Test-Driven Development in Go

Windi Chandra
LEARNFAZZ
Published in
5 min readFeb 27, 2019

In many projects, like our class project — LEARNFAZZ, we are encouraged to implement the usage of Test-Driven Development (TDD) in writing our code. Testing is necessary to ensure we are delivering a good product based on the requirements. For those unfamiliar with TDD, it is basically creating stubs and tests first before doing the implementation.

There are two main conditions in TDD, [RED] are for when we create stubs and test. The red comes from how our tests are red (failed) since we have not implemented the functions. [GREEN] for completing the implementation, the same idea as how our tests are green.

First we are creating stubs, consider the following code:

package sampletype SimpleCalculatorService struct{}func (s *SimpleCalculatorService) Add(a, b int) int {
return 0
}
func (s *SimpleCalculatorService) Subtract(a, b int) int {
return 0
}

We have created a stub for our SimpleCalculatorService, notice that the function doesn’t return the intended value. This is okay since our stub is only needed to allow code to compile successfully.

Next we are going to create tests, there is a really practical tool called gotests that can generate tests like these:

Generated TestSimpleCalculatorService_Add
Generated TestSimpleCalculatorService_Subtract
package sample
import "testing"func TestSimpleCalculatorService_Add(t *testing.T) {
type args struct {
a int
b int
}
tests := []struct {
name string
s *SimpleCalculatorService
args args
want int
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := SimpleCalculatorService{}
if got := s.Add(tt.args.a, tt.args.b); got != tt.want {
t.Errorf("SimpleCalculatorService.Add() = %v, want %v", got, tt.want)
}
})
}
}
func TestSimpleCalculatorService_Subtract(t *testing.T) {
type args struct {
a int
b int
}
tests := []struct {
name string
s *SimpleCalculatorService
args args
want int
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &SimpleCalculatorService{}
if got := s.Subtract(tt.args.a, tt.args.b); got != tt.want {
t.Errorf("SimpleCalculatorService.Subtract() = %v, want %v", got, tt.want)
}
})
}
}

In some cases, we might need to edit the generated code. For example in this case we might want to remove “s *SimpleCalculatorService” from our test struct since it is not used either.

What we need to do then is to fill the test cases. It is important to remember that our tests must cover all possible use cases to make sure the function do as we wanted but not too specific either. Our SimpleCalculatorService could have the following tests:

package sampleimport "testing"func TestSimpleCalculatorService_Add(t *testing.T) {
type args struct {
a int
b int
}
tests := []struct {
name string
args args
want int
}{
{
name: "b is positive",
args: args{
a: 1,
b: 2,
},
want: 3,
},
{
name: "b is zero",
args: args{
a: 5,
b: 0,
},
want: 5,
},
{
name: "b is negative",
args: args{
a: 3,
b: -4,
},
want: -1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := SimpleCalculatorService{}
if got := s.Add(tt.args.a, tt.args.b); got != tt.want {
t.Errorf("SimpleCalculatorService.Add() = %v, want %v", got, tt.want)
}
})
}
}
func TestSimpleCalculatorService_Subtract(t *testing.T) {
type args struct {
a int
b int
}
tests := []struct {
name string
args args
want int
}{
{
name: "b is positive",
args: args{
a: 1,
b: 2,
},
want: -1,
},
{
name: "b is zero",
args: args{
a: 5,
b: 0,
},
want: 5,
},
{
name: "b is negative",
args: args{
a: 3,
b: -4,
},
want: 7,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &SimpleCalculatorService{}
if got := s.Subtract(tt.args.a, tt.args.b); got != tt.want {
t.Errorf("SimpleCalculatorService.Subtract() = %v, want %v", got, tt.want)
}
})
}
}

We could then run our test and notice that our tests are failing since we are still in the [RED]. We could proceed to implement our function, and we should be in the [GREEN].

Taking It Further

In the practice, a function might not be so simple. It might call a service, access database, or call another function. Then how do we preserve the Isolation in F.I.R.S.T?

The answer is mocking. There are several ways to do it, one of them is via abstraction. The idea is to do not let a function implementation be called directly by a client. We can achieve this by creating an interface for our class thus the other objects do not need to know the implementation lying within and we can implement a mock class instead.

created with draw.io

Getting back to our SimpleCalculatorService, we could start by creating an abstraction:

package sampletype SimpleCalculatorService interface {
Add(a, b int) int
Subtract(a, b int) int
}
type SimpleCalculatorServiceImpl struct{}func (s *SimpleCalculatorServiceImpl) Add(a, b int) int {
return a + b
}
func (s *SimpleCalculatorServiceImpl) Subtract(a, b int) int {
return a - b
}

Notice that our previous struct is now called SimpleCalculatorServiceImpl since it has the real implementation of the class. We create an interface that will be used by client, the client will only call the functions through this interface. We could then create a mock class that implements our SimpleCalculatorService.

Another practical tool called mockgen could help in generating our mock class. Once generated, the mock class is ready to be used:

func Test_something(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mock := NewMockSimpleCalculatorService(ctrl) mock.EXPECT().Add(gomock.Any(), gomock.Any()).Return(1) // return 1 if our mock class' Add() is called
}

This is how TDD is implemented in our project, specifically the backend Go project. Hope it gives an idea on how to implement TDD for your projects.

EDIT:EXTRA

For those still not sure of how to replace the real class with mock class could check my article here. And here is an extra, notice that to mock a third-party library we could wrap the function then generate the mock class with mockgen, e.g:

func (s *SimpleCalculatorServiceImpl) Abs(x float64) {
return math.Abs(x)
}

Instead of calling math.Abs which we cannot mock, we could call our wrapper function thus could be mocked in our test. This method is actually quite an effort, some people might choose to avoid doing this but for the Isolation in F.I.R.S.T we will have to do this. Most cases are usually to mock database queries since we usually use libraries for database queries, e.g: sqlx. Wrapping functions would be a lot of work!

Luckily, there is another useful tool sqlmock that provides the database mock we need. It is also easy to use:

d, mock, _ := sqlmock.New() # d is *sql.DB
db := sqlx.NewDb(d, "postgres") # if you use sqlx, db is the database for your application
# you can then mock your database (using db) queries
mock.ExpectQuery("SELECT (.+) FROM users (.+)").WillReturnError(errors.New("test"))

Another plus is the provided mock functions receive a regex query string which is practical and could be tiresome to make by yourself. Your database queries will have the mock result when the queries match the regex.

--

--