Go Unittest by testify

For complex testing, the testify module is a big life saver, it has 4 main packages: assert, require, mock and suite. Their usages are quite straightforward from the testify README file.

There is a series of tutorial about how to use testify, pretty helpful.

Here in this blog I highlight the parts that are important to me.

Mock Objects Generation

Regarding mock, to create mocking objects for external dependencies, we use mockery to generate mock objects from go interface.

Thus to write testable code in Go, thinking about how to use interface properly.

Install mockery binary:

1
2
3
# Check current release tag and use it
# The mockery binary will be downloaded to $(go env GOPATH)/bin
go install github.com/vektra/mockery/v2@v2.20.0

Mock objects generation command:

1
2
3
4
5
6
7
# If you are using mockery binary downloaded, run it from the repo with absolute
# path.
# This will create a "mocks" folder in current directory and generate a mock
# file named as HelloWorld.go.

# HelloWorld is the interface name inside student folder
/<path to mockery parent folder>/mockery --dir student --name HelloWorld

Other flags please see mockery --help.

Or using docker:

1
2
3
docker run --rm -v $(pwd):/src \
-w /src \
vektra/mockery:v2.20.0 --dir student --name HelloWorld

Then in your test go file, import the mocks package and use it.

Suite

The suite helps the testing lifecycle management, for example, it provides setup/teardown stage for test cases.

For example the basic suite structure:

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
import (
"context"
"example.com/mocks"

"github.com/stretchr/testify/suite"
)

type ExploreWorldSuite struct {
suite.Suite
// from mocks package
hw *mocks.HelloWorld
// from current package
std *Student
// other utilities
ctx context.Context
}

// Reset for every test case
func (s *ExploreWorldSuite) SetupTest() {
// The HelloWorld.go mock file contains NewHelloWorld func
s.hw = mocks.NewHelloWorld(s.T())
s.ctx = context.TODO()
std = &Student {
Id: 1991,
Name: "cheng",
}
}

// All methods that begin with "Test" are run as tests within a suite.
func (s *ExploreWorldSuite) TestSayHello() {
// mock Say method inside the HelloWorld interface
s.hw.On("Say", mock.Anything, "World").Return(nil).Once()

// FirstTimeMeet calls Say method from HelloWorld interface
got := s.std.FirstTimeMeet(s.ctx)

// There are lots more helper methods, use properly
s.Require().True(got)
// This can be helpful if the calling func does not have return or obvious
// side effect
s.hw.AssertExpectations(s.T())
}

// In order for 'go test' to run this suite, we need to create a normal test
// function and pass our suite to suite.Run
func TestExploreWorldSuite(t *testing.T) {
suite.Run(t, new(ExploreWorldSuite))
}

The commonly used assertions:

1
2
3
4
5
6
7
8
s.Require().Nil(err)
s.Require().NotNil(err)
// although not recommended to rely on error message
s.Require().Contains(err.Error(), "xxxxxx")
s.Require().EqualError(err, "xxxxxx")

s.Require().Len(rcs, 5)
s.Require().EqualValues(pt, "xxxxxx")

Run Test

To run suite, using the same go test command:

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
# To run the whole package test suites
go test -v -buildvcs=false -mod=readonly \
example.com/student

# To run specific suite in package
go test -v -buildvcs=false -mod=readonly \
example.com/student \
-run <suite struct name regexp>

# to run test against the vendor folder
# -mod=vendor
go test -v -buildvcs=false -mod=vendor \
example.com/student \
-run <suite struct name regexp>

# to run specified tests in specific suite
# -run and -testify.m have to be after package
go test -v -buildvcs=false -mod=readonly \
example.com/student \
-run <suite struct name regexp> \
-testify.m <test name regexp>

# to run test without prior cache
# -count 1
go test -v -buildvcs=false -mod=readonly \
-count 1 \
example.com/student \
-run <suite struct name regexp> \
-testify.m <test name regexp>
0%