Table Driven Test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func TestAdd(t * testing.T) {
tcs := []struct{
Name string
A, B, Expected int
}{
{"foo", 1, 1, 2},
{"bar", 1, -1, 0},
}
for _, tc := range tcs {
t.Run(tc.Name, func(t *testing.T) {
actual := tc.A + tc.B
if actual != expected {
t.Errorf("%d + %d = %d, exected %d", tc.A, tc.B, actual, tc.Expected)
}
})
}
}

Test fixtures

go test set pwd as package directory
use relative path “test-fixtures” directory as a place to store test data(loading config, model data, binary data …etc)

1
2
3
4
func TestAdd(t *testing.T) {
data := filepath.Join("test-fixtures", "add_data.json")
// Do something with data
}

Golden files(Test flag)

expected output放在.golden file中
當update flag為true時, 代表需要更新expected output file
Very useful to test complex structures

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var update = flag.Bool("update", false, "update golden files")
func TestAdd(t *testing.T) {
// ... table (probably!)
for _, tc := range tcs {
actual := doSomething(tc)
golden := filepath.Join("test-fixtures", tc.Name+".golden")
if *update {
ioUtil.WriteFile(golden, actual, 0644)
}
expected, _ := ioutil.ReadFile(golden)
if !bytes.Equal(actual, expected) {
//FAIL!
}
}
}

1
2
go test
go test -update

Global State

Do not use global state.
If you have to do, Use default, and let test can easily mock it.

1
2
3
4
5
const defaultPort = 1000
type ServerOpts {
Port int // default it to defaultPort somewhere
}

Test Helpers

TestTempFile

Test helper接受testing.T參數, helper中的錯誤直接在helper中處理掉
helper return a function to clean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func testTempFile(t * testing.T) (string, func()) {
t.Helper()
tf, err := iotuil.TempFile("", "test")
if err != nil {
t.Fatalf("err: %s", err)
}
tf.Close()
return tf.Name(), func() { os.Remove(tf.Name()) }
}
func TestThing(t *testing T) {
tf, tfclose := testTempFile(t)
defer tfclose()
// doSomething with tf
}

TestChdir

切換dir並在function結束前切換回來
注意testChdir會回傳一function, 該function會將dir切回原本old
由於defer會在defer當下處理完參數所以在defer當下即會切換到dir
然後在TestThing結束前呼叫os.chdir(old)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func testChdir(t *testing.T, dir string) func() {
t.Helper()
old, err := os.Getwd()
if err != nil {
t.Fatalf("err: %s", err)
}
if err := os.Chdir(dir); err != nil {
t.Fatalf("err: %s", err)
}
return func() { os.Chdir(old) }
}
func TestThing(t *testing.T) {
defer testChdir(t, "/other")()
}

Networking

不需要mock net.Conn

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func TestConn(t *testing.T) (client, server net.Conn) {
t.Helper()
ln, err := net.Listen("tcp", "127.0.0.1:0")
var server net.Conn
go func() {
defer ln.Close()
server, err = ln.Acccept()
}()
client, err := net.Dial("tcp", ln.Addr().String())
return client, server
}

Test Subprocessing

Check if git is installed. If not, skip test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var testHasGit bool
func init() {
if _, err := exec.LookPath("git"); err == nil {
testHasGit = true
}
}
func TestGitGetter(t *testing.T) {
if !testHasGit {
t.Log("git not found, skipping")
t.Skip()
}
}

Mock Subprocess

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
func helperProcess(s ...string) *exec.Cmd {
cs := []string{"-test.run=TestHelperProcess", "--"}
cs = append(cs, s...)
env := []string{
"GO_WANT_HELPER_PROCESS=1"
}
cmd := exec.Command(os.Args[0], cs...)
cmd.Env = append(env, os.Environ()...)
return cmd
}
func TestHelperProcess(*testing.T) {
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
return
}
defer os.Exit(0)
args := os.Args
for len(args) > 0 {
if args[0] == "--" {
args = args[1:]
break
}
args = args[1:]
}
...
cmd, args := args[0], args[1:]
switch cmd {
case "foo":
// ...
}
}

Testing as a public API

提供使用你的package的user一個測試你的package的public API(Maybe a mock or a helper)
test.go will be ignored by go, so use testing.go for Test for API
Example:

  • API Server
    • TestServer(t) (net.Addr, io.Closer) => Returns a fully started in-memory server and a closer to close it.
  • interface for downloading files
    • TestDownloader(t, Downloader) => Tests all the properties a downloader should have.
    • struct DownloaderMock[] => Implements Downloader as a mock

Timing-Dependent Test

timeMultiplier is configurable.
maybe lower at laptop and higher at server

1
2
3
4
5
6
7
8
9
func TestThing(t * testing.T) {
timeout := 3 * time.Minute * timeMultiplier
select {
case <- thingHappened:
case <- time.After(timeout):
t.Fatal("timeout")
}
}

Complex Structs Compare(maybe graph)

How to compare complex structs in test:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type ComplexThing struct {/* ... */}
func (c *ComplexThing) testString() string {
// produce human-friendly output for test comparison
}
//----------------------------------------
func TestComplexThing(t *testing.T) {
c1, c2 := createComplexThings()
if c1.testString() != c2.testString() {
t.Fatalf("no match:\n\n%s\n\n%s", c1.testString(), c2.testString())
}
}