When writing tests, we all need to start external services such as Redis or Postgres, and we need to initialize the database connection before starting the test, or prepare the test data, and close the database connection after the test is finished, and remove unnecessary test data or files. In Golang, developers don’t need to rely on third-party packages, they can do this very easily with the built-in TestMain. Here’s how to do it and how to use it.

TestMain integration

The Go language provides the TestMain function directly in the test suite, which allows developers to prepare the environment (setup) before starting a test or remove it (teardown) after the test is finished. The following is an example of normal execution.

1
2
3
4
func TestMain(m *testing.M) {
  // call flag.Parse() here if TestMain uses flags
  os.Exit(m.Run())
}

Then you can add the setup() and teardown() functions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func setup() {
  // Do something here.
  fmt.Printf("\033[1;33m%s\033[0m", "> Setup completed\n")
}

func teardown() {
  // Do something here.
  fmt.Printf("\033[1;33m%s\033[0m", "> Teardown completed")
  fmt.Printf("\n")
}

func TestMain(m *testing.M) {
  setup()
  code := m.Run()
  teardown()
  os.Exit(code)
}

Finally, run go test -v . and you can see the following result.

1
2
3
4
5
> Setup completed
testing: warning: no tests to run
PASS
> Teardown completed
ok      test    0.299s

This is in line with our need to be able to prepare the environment before any test and remove the associated environment afterwards. At the bottom is the initialization of the Groutine Pool and the release of the connection at the end.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func setup() {
  // initial worker
  queue.New(cfg.Worker.NumProcs, cfg.Worker.MaxQueue).Run()
  fmt.Printf("\033[1;33m%s\033[0m", "> Setup completed\n")
}

func teardown() {
  // initial worker
  queue.Realse()
  fmt.Printf("\033[1;33m%s\033[0m", "> Teardown completed")
  fmt.Printf("\n")
}

Standalone testing using Setup and Teardown

In addition to being used before and after the overall test, developers may also need to test sub-testing. See the example below to test for user presence.

 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
func TestIsUserExist(t *testing.T) {
  assert.NoError(t, PrepareTestDatabase())
  type args struct {
    uid   int64
    email string
  }
  tests := []struct {
    name    string
    args    args
    want    bool
    wantErr bool
  }{
    {
      name:    "test email exist without login",
      args:    args{0, "test01@gmail.com"},
      want:    true,
      wantErr: false,
    },
    {
      name:    "test email not exist without login",
      args:    args{0, "test123456@gmail.com"},
      want:    false,
      wantErr: false,
    },
    {
      name:    "test email exist with login",
      args:    args{1, "test02@gmail.com"},
      want:    true,
      wantErr: false,
    },
    {
      name:    "test email not exist with login",
      args:    args{1, "test123456@gmail.com"},
      want:    false,
      wantErr: false,
    },
  }
  for _, tt := range tests {
    tt := tt
    t.Run(tt.name, func(t *testing.T) {
      got, err := IsUserExist(tt.args.uid, tt.args.email)
      if (err != nil) != tt.wantErr {
        t.Errorf("isUserExist() error = %v, wantErr %v", err, tt.wantErr)
        return
      }
      if got != tt.want {
        t.Errorf("isUserExist() = %v, want %v", got, tt.want)
      }
    })
  }
}

Then add the setupTest function.

1
2
3
4
5
6
7
func setupTest(tb testing.TB) func(tb testing.TB) {
  fmt.Printf("\033[1;33m%s\033[0m", "> Setup Test\n")

  return func(tb testing.TB) {
    fmt.Printf("\033[1;33m%s\033[0m", "> Teardown Test\n")
  }
}

Finally, modify the t.Run content.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
t.Run(tt.name, func(t *testing.T) {
  teardownTest := setupTest(t)
  defer teardownTest(t)
  got, err := IsUserExist(tt.args.uid, tt.args.email)
  if (err != nil) != tt.wantErr {
    t.Errorf("isUserExist() error = %v, wantErr %v", err, tt.wantErr)
    return
  }
  if got != tt.want {
    t.Errorf("isUserExist() = %v, want %v", got, tt.want)
  }
})

Integrating TestMain + Teardown

Let’s integrate the above cases together and write a simple ToString function first.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package foobar

import "fmt"

// ToString convert any type to string
func ToString(value interface{}) string {
  if v, ok := value.(*string); ok {
    return *v
  }
  return fmt.Sprintf("%v", value)
}

Then write the test.

 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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package foobar

import (
  "fmt"
  "os"
  "reflect"
  "testing"
)

func setup() {
  // Do something here.
  fmt.Printf("\033[1;33m%s\033[0m", "> Setup completed\n")
}

func teardown() {
  // Do something here.
  fmt.Printf("\033[1;33m%s\033[0m", "> Teardown completed")
  fmt.Printf("\n")
}

func TestMain(m *testing.M) {
  setup()
  code := m.Run()
  teardown()
  os.Exit(code)
}

func setupTest(tb testing.TB) func(tb testing.TB) {
  fmt.Printf("\033[1;34m%s\033[0m", ">> Setup Test\n")

  return func(tb testing.TB) {
    fmt.Printf("\033[1;34m%s\033[0m", ">> Teardown Test\n")
  }
}

func TestToString(t *testing.T) {
  type args struct {
    value interface{}
  }
  tests := []struct {
    name string
    args args
    want interface{}
  }{
    {
      name: "int",
      args: args{
        value: 101,
      },
      want: "101",
    },
    {
      name: "int64",
      args: args{
        value: int64(100),
      },
      want: "100",
    },
    {
      name: "boolean",
      args: args{
        value: true,
      },
      want: "true",
    },
    {
      name: "float32",
      args: args{
        value: float32(23.03),
      },
      want: "23.03",
    },
  }
  for _, tt := range tests {
    t.Run(tt.name, func(t *testing.T) {
      teardown := setupTest(t)
      defer teardown(t)
      if got := ToString(tt.args.value); !reflect.DeepEqual(got, tt.want) {
        t.Errorf("ToString() = %v, want %v", got, tt.want)
      }
    })
  }
}

The final results are as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ go test -v .
> Setup completed
=== RUN   TestToString
=== RUN   TestToString/int
>> Setup Test
>> Teardown Test
=== RUN   TestToString/int64
>> Setup Test
>> Teardown Test
=== RUN   TestToString/boolean
>> Setup Test
>> Teardown Test
=== RUN   TestToString/float32
>> Setup Test
>> Teardown Test
--- PASS: TestToString (0.00s)
    --- PASS: TestToString/int (0.00s)
    --- PASS: TestToString/int64 (0.00s)
    --- PASS: TestToString/boolean (0.00s)
    --- PASS: TestToString/float32 (0.00s)
PASS
> Teardown completed
ok      test    0.293s

Teardown