Go Notes
These are things I want to remember in Go
Notes on Testing
Some notes on testing in Go. A lot of these notes came from Advanced Testing in Go (Hashimoto) (transcipt).
Testing Private and Public APIs
Test files for a package's publicly visible API should be named <package>_ext_test.go
and start with package <package>_test
.
Test files for a package's internal API should be named <package>_int_test.go
and start with package <package>
Comparing Values Naming Convention
Go's testing
library wants you to do a lot of comparisons. The naming convention I want to use for these comparisons (taken from testify
) is to call the value that I expect expectedXXX
and always put it on the left side of the comparison, and the value that I actually got actualXXX
, and always put it on the right side of the comparison:
if expectedThing != actualThing {
t.Fatalf("Oopsie")
}
Degrees of Failure
The testing
library has a couple ways to fail:
Fail
marks the current test as failed, but continues the execution of the current test.
Error/Errorf
is equivalent to Log/Logf
followed by Fail
.
FailNow
marks the current test as failed, and stops execution of the current test.
Fatal/Fatalf
is equivalent to Log/Logf
followed by FailNow
.
testify
testify
is super nice for comparing things because it writes most of my if
statements for me. I can do:
// Mark current test as failed, but continue current test
assert.Equal(t, expectedValue, actualValue)
// Mark current test as failed, exit current test
require.Equal(t, expectedValue, actualValue)
Table Driven Tests
Test the same logic on different data!
Here's a simple example - note that in lieu of testing what's in the potential error, I simply assert that it's nil or not nil. For this particular, this is "enough" to satisfy me. Other tests might require more detailed comparisons.
package main
import (
"errors"
"testing"
"github.com/stretchr/testify/require"
)
func AddOne(a int) (int, error) {
if a == 3 {
return 0, errors.New("We don't like the number 3")
}
return a + 1, nil
}
func TestAddOne(t *testing.T) {
tests := []struct {
name string
a int
expectedSum int
expectedErr bool
}{
{name: "first", a: 1, expectedSum: 2, expectedErr: false},
{name: "second", a: 2, expectedSum: 4, expectedErr: false},
{name: "third", a: 3, expectedSum: 0, expectedErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actualSum, actualErr := AddOne(tt.a)
if tt.expectedErr {
require.NotNil(t, actualErr)
} else {
require.Nil(t, actualErr)
}
require.Equal(t, tt.expectedSum, actualSum)
})
}
}
When run, we see that the function is incorrect for the test data provided (or, more likely in this case, we need to correct the test data). testify
gives us a super helpful error message.
$ go test ./...
--- FAIL: TestAddOne (0.00s)
--- FAIL: TestAddOne/second (0.00s)
main_test.go:37:
Error Trace: main_test.go:37
Error: Not equal:
expected: 4
actual : 3
Test: TestAddOne/second
FAIL
FAIL github.com/bbkane/hello_testing 0.173s
FAIL
Data Files
When a test depends on a data file, Go will read it from a file path relative to the test. I like to stick my files for a test in a testdata/TestName.xxx
file right next to the test. Then, within the test, I can ioutil.Readfile('testdata/TestName.xxx')
to get the data. If each sub test in a table-driven test, needs a file, then I use testdata/TestName/SubTestName.xxx
.
Golden Files
Sometimes, I want to test that a function outputs the correct bytes - like a file, an HTTP response, or the --help
output from the CLI parsing library I'm writing. In these cases, it's helpful to make the tests take an -update
flag, and write the bytes to a file when it is passed. Then I can manually read those files to ensure correctness, commit them, and make the test check the bytes generated to the file when the -update
flag is not passed. An example:
package main_test
import (
"bytes"
"flag"
"io"
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
)
func Write(w io.Writer) error {
_, err := w.Write([]byte("hola\n"))
return err
}
var update = flag.Bool("update", false, "update golden files")
func TestWrite(t *testing.T) {
var actualBuffer bytes.Buffer
actualErr := Write(&actualBuffer)
require.Nil(t, actualErr)
golden := filepath.Join("testdata", t.Name()+".golden.txt")
if *update {
mkdirErr := os.MkdirAll("testdata", 0700)
require.Nil(t, mkdirErr)
writeErr := ioutil.WriteFile(golden, actualBuffer.Bytes(), 0600)
require.Nil(t, writeErr)
t.Logf("Wrote: %v\n", golden)
}
expectedBytes, readErr := ioutil.ReadFile(golden)
require.Nil(t, readErr)
require.Equal(t, expectedBytes, actualBuffer.Bytes())
}
When running this test the first time, I get the following error:
$ go test golden_test.go
--- FAIL: TestWrite (0.00s)
golden_test.go:37:
Error Trace: golden_test.go:37
Error: Expected nil, but got: &fs.PathError{Op:"open", Path:"testdata/TestWrite.golden.txt", Err:0x2}
Test: TestWrite
FAIL
FAIL command-line-arguments 0.095s
FAIL
However, I can then use the -update
flag to write the file (I'm also using the -test.v
flag to show the log I have)
$ go test golden_test.go -test.v -update
=== RUN TestWrite
golden_test.go:32: Wrote: testdata/TestWrite.golden.txt
--- PASS: TestWrite (0.00s)
PASS
ok command-line-arguments 0.090s
Then manually inspect it to ensure the function is correct:
$ cat testdata/TestWrite.golden.txt
hola
Then run the test again to see it pass:
$ go test golden_test.go
ok command-line-arguments 0.096s
Code examples
Code examples can be added to tests and also show up in the docs! See the Go blog for more details, or see my example below:
package main_test
import "fmt"
func ExampleExample() {
fmt.Println("hello!")
// Output: hello!
}