Writing unit test cases helps you to test your software before release which improves your confidence to
publish / release / launch your product….and bla bla bla. You can find more information over the internet
as this post mainly focus on writing unit tests in golang.
Go has in-built package for writing unit tests, testing
. Along with this, we use following couple of libs / tools for
writing test suites, supporting setup / teardown hooks and for mocking interfaces.
suite package – Helps us to construct test suites.
gomock package – Mocking framework useful to mock api / db calls.
[ Writing Tests ]
Let us consider following basic go code of ToyStore to sell toys. ToyStore interface provides the BuyToy functionality
by reading session context and given toy id. ToyStore depends on the api package which records the purchase under the hood.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| package toystore
import (
"context"
"github.com/siva-chegondi/learning/golang/learning/toystore/api"
"log"
)
type ToyStore interface {
BuyToy(toyId int, sessionCtx context.Context) error
}
var logger = log.Default()
type toystore struct {
storeAPI api.StoreAPI
}
func NewToyStore(storeAPI api.StoreAPI) ToyStore {
return &toystore{storeAPI: storeAPI}
}
// BuyToy
// Method to buy a toy from the store
// by user from sessionCtx
func (t toystore) BuyToy(toyId int, sessionCtx context.Context) error {
logger.Printf("Starting purchase of toy %v from the store", toyId)
userId := sessionCtx.Value("user-id")
return t.storeAPI.BuyToy(toyId, userId.(int))
}
|
Following is the service code which is performing API call to record purchase of a toy.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| package api
import (
"encoding/json"
"net/http"
"strings"
)
type StoreAPI interface {
BuyToy(toyId, userId int) error
}
type PurchaseRecord struct {
purchaseId string
toyId int
userId int
}
type APIClient struct {
httpClient *http.Client
}
func (api *APIClient) BuyToy(toyId, userId int) error {
data, _ := json.Marshal(&PurchaseRecord{
purchaseId: "dummy-unique-uuid",
toyId: toyId,
userId: userId,
})
reader := strings.NewReader(string(data))
_, err := api.httpClient.Post("/api/toystore/buy", "application/json", reader)
return err
}
|
To write unit tests for above ToyStore interface, first we generate mocks to the service code doing API calls.
For that, we install mockgen
CLI and run following commands to generate mocks for service code.
$ # Run following command to install mockgen
$ go install go.uber.org/mock/mockgen@latest
$
$ # Run following command to add gomock as dependency
$ go get go.uber.org/mock/gomock
$
$ # Run following command to generate mocks for the interface
$ mockgen -source toystore/api/api.go -destination toystore/mock_api/api_mock.go -package mock_api
Above commands should generate mock something similar to this.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
| // Code generated by MockGen. DO NOT EDIT.
// Source: toystore/api/api.go
//
// Generated by this command:
//
// mockgen -source toystore/api/api.go -destination toystore/mock_api/api_mock.go
//
// Package mock_api is a generated GoMock package.
package mock_api
import (
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockStoreAPI is a mock of StoreAPI interface.
type MockStoreAPI struct {
ctrl *gomock.Controller
recorder *MockStoreAPIMockRecorder
isgomock struct{}
}
// MockStoreAPIMockRecorder is the mock recorder for MockStoreAPI.
type MockStoreAPIMockRecorder struct {
mock *MockStoreAPI
}
// NewMockStoreAPI creates a new mock instance.
func NewMockStoreAPI(ctrl *gomock.Controller) *MockStoreAPI {
mock := &MockStoreAPI{ctrl: ctrl}
mock.recorder = &MockStoreAPIMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockStoreAPI) EXPECT() *MockStoreAPIMockRecorder {
return m.recorder
}
// BuyToy mocks base method.
func (m *MockStoreAPI) BuyToy(toyId, userId int) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BuyToy", toyId, userId)
ret0, _ := ret[0].(error)
return ret0
}
// BuyToy indicates an expected call of BuyToy.
func (mr *MockStoreAPIMockRecorder) BuyToy(toyId, userId any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BuyToy", reflect.TypeOf((*MockStoreAPI)(nil).BuyToy), toyId, userId)
}
|
Using the testing
package along with the generated mocks, we can write unit tests for the ToyStore
interface with the help of the suite
library.
To install the suite
library, use the following command:
$ go get github.com/stretchr/testify/suite
Now follow below steps to test the interface.
- Create a test suite structure by embedding the
suite.Suite
struct. - Include all testable interfaces and their corresponding mocks as part of the test suite struct.
- In the
Setup
method, initialize the gomock.Controller
instance, which will be used to manage the creation and lifecycle of mock objects. - Use the mock instance to initialize the
ToyStore
interface for testing. - Leverage the generated mocks to define expected behavior and results while writing test cases.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| package toystore
import (
"context"
"github.com/siva-chegondi/learning/golang/learning/toystore/mock_api"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"go.uber.org/mock/gomock"
"testing"
)
type ToyStoreTestSuite struct {
suite.Suite
ctrl *gomock.Controller
apiClient *mock_api.MockStoreAPI
toyStore ToyStore
}
func (s *ToyStoreTestSuite) SetupSuite() {
s.ctrl = gomock.NewController(s.T())
s.apiClient = mock_api.NewMockStoreAPI(s.ctrl)
s.toyStore = NewToyStore(s.apiClient)
}
func (s *ToyStoreTestSuite) TearDownSuite() {
s.ctrl.Finish()
}
func (s *ToyStoreTestSuite) TestBuyToy() {
// mock api call to the expected result
s.apiClient.EXPECT().BuyToy(gomock.Any(), gomock.Any()).Return(nil)
err := s.toyStore.BuyToy(100, context.WithValue(context.Background(), "user-id", 213))
assert.Nil(s.T(), err)
}
func TestToyStoreSuite(t *testing.T) {
suite.Run(t, new(ToyStoreTestSuite))
}
|
TestToyStoreSuite
is the main method which will run by go test
command which in turn runs all subtests in the suite.