Testing in Go is very easy and well designed. All test codes are next to the source codes in every package and they facilitate both black-box and white-box testing at the developer's disposal.
testYourNameHere
.func TestFunctionA(t *testing.T) {
.All test scripts should follow the source codes name with the ending _test
before the extension. Example: sourceCodeName_test.go
for sourceCodeName.go
.
If you read the Go documentation, you will immediately realize that they recommend you to implement a table-driven testing. Basically, the idea of such testing is that you always test against the Public API (most of the time) by feeding it a range of possible parameters. If you practice Effective Go and have a good basic use of the language, you can basically manipulate and covers all the source codes by this table-driven method.
The code pattern is basically as such:
func TestYourFunction(t *testing.T) {
// formulate your test values
<setup your common test values>
// setup the table
scenarios := []struct {
inputName <type>
...
outputName <type>
} {
{
inputName: <value>,
...,
outputName: <value>,
}, {
inputName: <value>,
...,
outputName: <value>,
}, {
...
inputName: <value>,
...,
outputName: <value>,
}
}
// run test
for caseNumber, scenario := range scenarios {
// assemble and run test
// report log
// report result
}
}
This way, you can just manipulate the test values without performing drastic changes over your function. The only downside however, is that if you have a detailed table of values, the function can be very long. Hence, this forces you to test smartly and effectively (rather than chucking in a lot of values).
Here is a full example of a test suite:
func TestNaClSymmetricEncrypt(t *testing.T) {
commonPayload := []byte("En: A quick brown fox jumps over the lazy dog")
largePayload := make([]byte, 16001)
for i := 1; i <= 16000; i++ {
largePayload[i] = 55
}
nonePayload := []byte("")
commonKey := make([]byte, 32)
_, err := rand.Read(commonKey)
if err != nil {
t.Errorf("FAIL: unable to prepare common key - %v", err)
return
}
longKey := make([]byte, 64)
_, err = rand.Read(longKey)
if err != nil {
t.Errorf("FAIL: unable to prepare long key - %v", err)
return
}
shortKey := make([]byte, 16)
_, err = rand.Read(shortKey)
if err != nil {
t.Errorf("FAIL: unable to prepare short key - %v", err)
return
}
noneKey := []byte{}
scenarios := []struct {
inID int
inPayload *[]byte
inKey *[]byte
inBadRand bool
outError bool
outData bool // we just test the out generated action, not algo
}{
{
inID: 0,
inBadRand: false,
inPayload: &commonPayload,
inKey: &commonKey,
outError: false,
outData: true,
}, {
inID: 1,
inBadRand: false,
inPayload: &nonePayload,
inKey: &commonKey,
outError: false,
outData: true,
}, {
inID: 2,
inBadRand: false,
inPayload: &largePayload,
inKey: &commonKey,
outError: true,
outData: false,
}, {
inID: 3,
inBadRand: false,
inPayload: &commonPayload,
inKey: &longKey,
outError: true,
outData: false,
}, {
inID: 4,
inBadRand: false,
inPayload: &commonPayload,
inKey: &shortKey,
outError: true,
outData: false,
}, {
inID: 5,
inBadRand: false,
inPayload: &commonPayload,
inKey: &noneKey,
outError: true,
outData: false,
}, {
inID: 6,
inBadRand: true,
inPayload: &commonPayload,
inKey: &commonKey,
outError: true,
outData: false,
},
}
cipher := NaCl{}
oriReader := rand.Reader
badReader := new(testRand)
for _, scenario := range scenarios {
rand.Reader = oriReader
if scenario.inBadRand {
rand.Reader = badReader
}
data, err := cipher.SymmetricEncrypt(scenario.inPayload,
scenario.inKey)
t.Logf(`
CASE %v
GIVEN:
payload=%v
key=%v
EXPECT:
error=%v
GOT:
data=%v
error=%v
`,
scenario.inID,
scenario.inPayload,
scenario.inKey,
scenario.outError,
data,
err)
if (scenario.outError && err == nil) ||
(!scenario.outError && err != nil) {
t.Errorf("FAIL: unexpected error")
}
if (scenario.outData && data == nil) ||
(!scenario.outData && data != nil) {
t.Errorf("FAIL: unexpected encryption")
}
}
}
TIP:
t.Logf
to report the test case data instead of squeezing them inside t.Errorf
. Now that you have the test script ready, we can run the test. There are a few ways to do it.
The easiest way to do it is:
$ go test .
If you want to do it recursively:
$ go test ./...
This will run the test quick and easy way.
There are various way execute the test with coverage, notably using the -cover
. However, to me, that statistic itself is useless. Fortunately, Go provides a "code coverage heat mapping" way which I adore and use till today.
Therefore, I will recommend only this way since:
At this point of writing (Go 1.12.1), It is still 2 lines commands. Here they are:
$ go test -coverprofile /tmp/gotest.out -v .
$ go tool cover html=/tmp/gotest.out -o /tmp/gotest.html
$ xdg-open /tmp/gotest.html # or just use the browser to open the html result
/test/gotest.out
./tmp/gotest.html
.If you do everything correctly, you get a heat map of your source codes, like this below, and continue to develop your test script strategically:
Fortunately after Go 1.11 onward, go test facilitates race detector which is a very important tool and simplified a lot of test implementations. Although you need to write your test code to test concurrency implementation, consider using this race detector to simplify a bunch of tracking. All you need to do is to append the -race
argument. Example:
$ go test -coverprofile /tmp/gotest.out -race .
OR
$ go test -cover -race .
$ go test -cover -race ./...
$ go test -cover -race "${HOME}/Documents/myproject/...
In some cases, linter do fails to provide true positive results. Once in a while, you get a bunch of false positive reports. In those situations, you want the linter to skip the scan for particular line, section, or the entire package itself (source: https://github.com/golangci/golangci-lint#nolint).
To do that, simply append machine readable instruction into the scope. Example:
var bad_name int //nolint:golint,unused
//nolint
func allIssuesInThisFunctionAreExcluded() *string {
// ...
}
//nolint:govet
var (
a int
b int
)
//nolint:unparam
package pkg
NOTE:
Now that we have our desired tools (so far), it's time to bundle them together and make it a one line command. For me on Debian, I wrapped all the commands into a function and exported it out as a BASH function. Then I save this function in my ~/.bashrc
. Example:
gotest() {
open="false"
arg="${1:-.}"
if [ "$1" == "-r" ]; then
arg="$2"
open="true"
fi
go test -coverprofile /tmp/gotest.out -race -v "$arg" \
| tee /tmp/gotest.log
if [ $? != 0 ] ;then
return 1
fi
2>&1 go tool cover \
-html=/tmp/gotest.out \
-o /tmp/gotest.html \
> /dev/null
if [ "$open" == "true" ]; then
xdg-open /tmp/gotest.html &> /dev/null
xdg-open /tmp/gotest.log &> /dev/null
fi
}
export -f gotest
Then, what I do is that I just have to call gotest
to the place I want. Example:
$ gotest
$ gotest .
$ gotest ./...
$ gotest "${HOME}/Document/myproject/..."
If I want to see the detailed report, I just pass in the -r argument before the pathing:
$ gotest -r
$ gotest -r .
$ gotest -r ./...
$ gotest -r "${HOME}/Document/myproject/..."
That's all about testing in Go.