programming

Techniques for Creating Mocking Unit Tests in Golang

When we create a function or code, sometimes we have difficulty carrying out unit tests at several points that we cannot cover with unit tests. So here are several technical ways to carry out unit tests using the mocking technique. But actually we can also use third-party which is already available in several libraries so we just need to use it straight away.

Well, the drawback is that when we use Third-party we don’t fully understand the process of the Third-party unit test to carry out covering unit tests. So, we also need to know how to do mocking so we can see the flow of the code process that we are running.

Higher-Order Functions

Suppose we have a function to connect to a SQL database as below.

func OpenDB(user, password, addr, db string) (*sql.DB, error) {
	conn := fmt.Sprintf("%s:%s@%s/%s", user, password, addr, db)
	sql, err :=sql.Open("mysql", conn)
    if err != nil {
        log.Error("error open connection mysql")
    }
    return sql, nil  
}

So that we can test the function from sql.Open, we need to make changes to our code, namely by mock the function to a function type. To make it easier, you can see the implementation below.

type (
	sqlOpener func(string, string) (*sql.DB, error)
)

func OpenDB(user, password, addr, db string, open sqlOpener) (*sql.DB, error) {
	conn := fmt.Sprintf("%s:%s@%s/%s", user, password, addr, db)
	sql, err := open("mysql", conn)
    if err != nil {
        log.Error("error open connection mysql")
    }
    return sql, nil
}

In the sqlOpener type, we will mock the function for unit test needs later so that we can create test cases for errors to occur and succeed.

When calling the OpenDB function we need to send the sql.Open function so that it can provide according to the main function. To better understand how it is implemented, we can look at the code below.

    OpenDB("myUser", "myPass", "localhost", "foo", sql.Open)

So how do we create unit tests? Please take a look and see the implementation below.

func TestOpenDB(t *testing.T) {
	type args struct {
		user     string
		password string
		addr     string
		db       string
		open     func(string, string) (*sql.DB, error)
	}
	tests := []struct {
		name    string
		args    args
		wantErr bool
	}{
		{
			name: "case 1 : success open connection database mysql",
			args: args{
				user:     "myUser",
				password: "myPass",
				addr:     "localhost",
				db:       "foo",
				open: func(s1, s2 string) (*sql.DB, error) {
					return &sql.DB{}, nil
				},
			},
			wantErr: false,
		},
		{
			name: "case 2: failed open connection because have error",
			args: args{
				user:     "myUser",
				password: "myPass",
				addr:     "localhost",
				db:       "foo",
				open: func(s1, s2 string) (*sql.DB, error) {
					return nil, errors.New("got error")
				},
			},
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			_, err := OpenDB(tt.args.user, tt.args.password, tt.args.addr, tt.args.db, tt.args.open)
			if (err != nil) != tt.wantErr {
				t.Errorf("OpenDB() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
		})
	}
}

We need to pay attention to this method when we create a mock for the original function, because it could be that when we upgrade the dependency, some parameters change or there are additions, so we also need to change all the function variables created by the ‘mock’ so that we can adjust the function. .

Monkey Patching

This technique is almost the same as the mock Higher-Order Functions technique, in fact it is very similar to this technique, namely that we will make the main function that will be called sql.Open into a global variable.

Instead of passing the function to OpenDB(), we just use the variable for the actual call. Below is the implementation in the code.

var (
	SQLOpen = sql.Open
)

func OpenDB(user, password, addr, db string) (*sql.DB, error) {
	conn := fmt.Sprintf("%s:%s@%s/%s", user, password, addr, db)
	sql, err := SQLOpen("mysql", conn)
	if err != nil {
		log.Print("error open connection mysql")
		return sql, err
	}

	return sql, nil
}

The only difference is the data type used, namely the initialization of variables for this technique. Then, how do you mock in the unit test? Below we will explain.

for _, tt := range tests {
    t.Run(tt.name, func(t *testing.T) {
        SQLOpen = tt.args.open
        _, err := OpenDB(tt.args.user, tt.args.password, tt.args.addr, tt.args.db)
        if (err != nil) != tt.wantErr {
            t.Errorf("OpenDB() error = %v, wantErr %v", err, tt.wantErr)
            return
        }
    })
}

And in the unit test section, the difference is that we do assign to the SQLOpen variable which is mocked from each test case so that it can describe the error or success case.

Sometimes this technique is also not the best way to improve unit test coverage because you need to make sure the variable is public so it can be called by the main function.

Remember! This technique is the same as the previous technique, so we need to be careful when using it when we want to upgrade a third party, so we need to make sure that the mock function must be adjusted again if there are changes to the original dependency.

Interface Substitution

We use this technique for interface or concrete function types. In the Go language, we can do this technique by having an interface function so there is no need to implicitly implement the function.

Sometimes we need to do this interface in order to reduce the range of unit tests we will test. For example, let’s take an example of creating a function to retrieve data from a file like the one below.

package main

import (
	"fmt"
	"os"
)

func main() {
	f, err := os.Open("foo.txt")
	if err != nil {
		fmt.Printf("error opening file %v \n", err)
	}
	data, err := ReadContents(f, 50)
	if err != nil {
		fmt.Printf("error from ReadContents %v \n", err)
	}
	fmt.Printf("data from file: %s", string(data))
}

func ReadContents(f *os.File, numBytes int) ([]byte, error) {
	defer f.Close()
	data := make([]byte, numBytes)
	_, err := f.Read(data)
	if err != nil {
		return nil, err
	}
	return data, nil
}

We need to emulate the function in os.File, namely we use the ReadContents function. Specifically we use the f.Read(data) function to read data from the file and end with us closing the file with defer f .Clode()

That way we will create a mock from os.File which is the standard IO library package from Golang. can be seen below

type Reader interface {
	Read(p []byte) (n int, err error)
}

type Closer interface {
	Close() error
}

// ReadCloser is the interface that groups the basic Read and Close methods.
type ReadCloser interface {
	Reader
	Closer
}

Because os.File is an implementation of the io library, we can change the ReadContents function to something like the one below.

func ReadContents(rc io.ReadCloser, numBytes int) ([]byte, error) {
    defer rc.Close()
    data := make([]byte, numBytes)
    _, err := rc.Read(data)
    if err != nil {
        return nil, err
    }
    return data, nil
}

In most cases, we will probably need to create our own interface, but here we can reuse the interface defined in the io package. Now we try to create unit tests easily using mock.

package main

import (
	"errors"
	"io"
	"reflect"
	"testing"
)

type (
	mockReadCloser struct {
		expectedData []byte
		expectedErr  error
	}
)

func (mrc *mockReadCloser) Read(p []byte) (n int, err error) {
	copy(p, mrc.expectedData)
	return 0, mrc.expectedErr
}

func (mrc *mockReadCloser) Close() error { return nil }

func TestReadContents(t *testing.T) {
	errorz := errors.New("got error")
	type args struct {
		rc       io.ReadCloser
		numBytes int
	}
	tests := []struct {
		name         string
		args         args
		expectedData []byte
		expectedErr  error
	}{
		{
			name: "case success getting data read",
			args: args{
				rc: &mockReadCloser{
					expectedData: []byte(`hello`),
					expectedErr:  nil,
				},
				numBytes: 5,
			},
			expectedData: []byte(`hello`),
			expectedErr:  nil,
		},
		{
			name: "case failed getting data read",
			args: args{
				rc: &mockReadCloser{
					expectedData: []byte(`hello`),
					expectedErr:  errorz,
				},
				numBytes: 5,
			},
			expectedData: nil,
			expectedErr:  errorz,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := ReadContents(tt.args.rc, tt.args.numBytes)
			if !reflect.DeepEqual(got, tt.expectedData) {
				t.Errorf("expected (%b), got (%b)", tt.expectedData, got)
			}
			if !errors.Is(err, tt.expectedErr) {
				t.Errorf("expected error (%v), got error (%v)", tt.expectedErr, err)
			}
		})
	}
}

Please note that struct mockReadCloser is a mock of the interface, this way, each test can create a struct and return values as desired.

Embedding Interfaces

Embedding Interface is a mocking technique using embedded interface functions that we create as if the implementation matches our expectations. Here we use the AWS SDK Library which we can use to do unit testing.

The following is an example code, for example, we use AWS Dynamodb to retrieve batch item data.

package main

import (
	"log"

	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/dynamodb"
	"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface"
)

func main() {
	sess := session.New()
	svc := dynamodb.New(sess)

	GetBatchItem(svc, &dynamodb.BatchGetItemInput{
		RequestItems: map[string]*dynamodb.KeysAndAttributes{
			"a": &dynamodb.KeysAndAttributes{
				AttributesToGet: []*string{},
			},
		},
	})
}

func GetBatchItem(svc dynamodbiface.DynamoDBAPI, input *dynamodb.BatchGetItemInput) (*dynamodb.BatchGetItemOutput, error) {
	batch, err := svc.BatchGetItem(input)
	if err != nil {
		log.Printf("error")
		return nil, err
	}

	return batch, nil
}

The complete unit test looks like this where we create a mockDynamoDBClient struct containing the dynamodbiface.DynamoDBAPI interface which has several methods. What we mock is only the method we need, namely the BatchGetItem method, so we don’t need to implement everything.

package main

import (
	"errors"
	"testing"

	"github.com/aws/aws-sdk-go/service/dynamodb"
	"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface"
)

type mockDynamoDBClient struct {
	dynamodbiface.DynamoDBAPI
}

func (m *mockDynamoDBClient) BatchGetItem(d *dynamodb.BatchGetItemInput) (*dynamodb.BatchGetItemOutput, error) {
	if len(d.RequestItems) == 0 {
		return nil, errors.New("got error")
	}
	return &dynamodb.BatchGetItemOutput{
		Responses: map[string][]map[string]*dynamodb.AttributeValue{},
	}, nil
}

func TestGetBatchItem(t *testing.T) {
	type args struct {
		svc   dynamodbiface.DynamoDBAPI
		input *dynamodb.BatchGetItemInput
	}
	tests := []struct {
		name    string
		args    args
		wantErr bool
	}{
		{
			name: "success get batch items",
			args: args{
				svc: &mockDynamoDBClient{},
				input: &dynamodb.BatchGetItemInput{
					RequestItems: map[string]*dynamodb.KeysAndAttributes{
						"a": {
							AttributesToGet: []*string{},
						},
					},
				},
			},
		},
		{
			name: "failed get batch items",
			args: args{
				svc:   &mockDynamoDBClient{},
				input: &dynamodb.BatchGetItemInput{},
			},
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			_, err := GetBatchItem(tt.args.svc, tt.args.input)
			if (err != nil) != tt.wantErr {
				t.Errorf("GetBatchItem() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
		})
	}
}

Mocking out Downstream HTTP Calls

Creating mocks for external HTTP calls is a bit tricky if we want to implement them. but with this technique we can completely cover all the cases that we will make.

Suppose we have a function which will access an external Rest API, more details as follows.

type Response struct {
    ID          int    `json:"id"`
    Name        string `json:"name"`
    Description string `json:"description"`
}

func MakeHTTPCall(url string) (*Response, error) {
    resp, err := http.Get(url)
    if err != nil {
        return nil, err
    }
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return nil, err
    }
    r := &Response{}
    if err := json.Unmarshal(body, r); err != nil {
        return nil, err
    }
    return r, nil
}

Nah lalu bagaimana caranya agar bisa kita buat unit test-nya? Ini biasnaya kita menggunakan httptest library standar-nya dari Golang yang nantinya seolah-olah bisa membuat API external dengan response yang disesuaikan.

Lebih lengkapnya yuk kita coba langkah-langkahnya sebagai berikut.

  • Arahkan kursor pada fungsi MakeHTTPCall lalu klik kanan dan pilih Go: Generate Unit Tests For Function, maka akan dilakukan generate code default unit test seperti ini.
func TestMakeHTTPCall(t *testing.T) {
		type args struct {
				url string
		}
		tests := []struct {
				name    string
				args    args
				want    *Response
				wantErr bool
		}{
				// TODO: Add test cases.
		}
		for _, tt := range tests {
				t.Run(tt.name, func(t *testing.T) {
						got, err := MakeHTTPCall(tt.args.url)
						if (err != nil) != tt.wantErr {
								t.Errorf("MakeHTTPCall() error = %v, wantErr %v", err, tt.wantErr)
								return
						}
						if !reflect.DeepEqual(got, tt.want) {
								t.Errorf("MakeHTTPCall() = %v, want %v", got, tt.want)
						}
				})
		}
}
  • Next, add to the struct args this variable server *httptest.Server which functions to create mock external http call data.
  • Then below before calling MakeHTTPCall something needs to be updated like this
defer tt.args.server.Close()
var url string
if tt.args.url == "" {
		url = tt.args.server.URL
}
got, err := MakeHTTPCall(url)

Information:

  1. defer tt.args.server.Close() is intended so that for each NewServer test we need to close the server so it doesn’t conflict.
  2. var url string is used to check whether the url is true or false
  • Finally, we add the test cases that we need according to the code we created.
{
		name: "success call http",
		args: args{
				server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
						w.WriteHeader(http.StatusOK)
						w.Write([]byte(`{"id": 1, "name": "santekno", "description": "santekno jaya"}`))
				})),
		},
		want: &Response{
				ID:          1,
				Name:        "santekno",
				Description: "santekno jaya",
		},
		wantErr: false,
},
{
		name: "failed call http when http 400",
		args: args{
				server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
						w.WriteHeader(http.StatusBadRequest)
				})),
		},
		want:    nil,
		wantErr: true,
},
{
		name: "failed url http call",
		args: args{
				url: "localhost",
				server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
						w.WriteHeader(http.StatusBadRequest)
				})),
		},
		want:    nil,
		wantErr: true,
},

Everything is filled in, we just have to try to run whether each test case covers all of our code or not.

Want to know more about the unit test code? Here we will provide more detailed information

func TestMakeHTTPCall(t *testing.T) {
	type args struct {
		url    string
		server *httptest.Server
	}
	tests := []struct {
		name    string
		args    args
		want    *Response
		wantErr bool
	}{
		{
			name: "success call http",
			args: args{
				server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
					w.WriteHeader(http.StatusOK)
					w.Write([]byte(`{"id": 1, "name": "santekno", "description": "santekno jaya"}`))
				})),
			},
			want: &Response{
				ID:          1,
				Name:        "santekno",
				Description: "santekno jaya",
			},
			wantErr: false,
		},
		{
			name: "failed call http when http 400",
			args: args{
				server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
					w.WriteHeader(http.StatusBadRequest)
				})),
			},
			want:    nil,
			wantErr: true,
		},
		{
			name: "failed url http call",
			args: args{
				url: "localhost",
				server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
					w.WriteHeader(http.StatusBadRequest)
				})),
			},
			want:    nil,
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			defer tt.args.server.Close()
			var url string
			if tt.args.url == "" {
				url = tt.args.server.URL
			}
			got, err := MakeHTTPCall(url)
			if (err != nil) != tt.wantErr {
				t.Errorf("MakeHTTPCall() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("MakeHTTPCall() = %v, want %v", got, tt.want)
			}
		})
	}
}

Conclusion

If we don’t study it manually, we won’t know how the unit test works, so it is hoped that before we use Third-Party which supports the fulfillment of unit tests, it would be a good idea for us to also know how the mechanism works.

The purpose of unit tests is actually to test whether our code meets business needs, the product we are developing and so that there are minimal bugs when we run it in production (live).

comments powered by Disqus