在 Go 語言測試使用 Setup 及 Teardown

logo

相信大家在寫測試時,都會需要啟動而外服務,像是 RedisPostgres 等,而開始測試前會需要初始化資料庫連線,或者是準備測試資料,測試結束後就關閉資料庫連線,並且移除不必要的測試資料或檔案。在 Go 語言內開發者不用去依賴第三方的套件,透過內建的 TestMain 就可以非常輕鬆完成此事情。底下看看如何操作及使用。

整合 TestMain

Go 語言在測試套件內直接提供了 TestMain 函式,功能就是讓開發者可以在開始測試前準備環境 (setup) 或是測試結束後移除環境 (teardown)。底下看看正常執行範例

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

接著可以新增 setup()teardown() 函式

 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)
}

最後執行 go test -v . 後可以看到底下結果

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

這是符合我們的需求,可以在任何測試前準備環境,結束後可以移除相關環境,底下就是初始化 Groutine Pool,結束後釋放連線。

 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")
}

單獨測試使用 Setup 及 Teardown

除了在整體測試前及測試後需要使用外,開發者也可能有需求在測試子項目 (sub-testing) 上。直接看底下範例,測試使用者是否存在

 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)
      }
    })
  }
}

接著新增 setupTest 函式

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")
  }
}

最後修改 t.Run 內容

 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)
  }
})

整合 TestMain + Teardown

我們來將上述的案例整合一起使用,先寫一個簡單的 ToString 功能

 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)
}

接著寫測試

 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)
      }
    })
  }
}

最後結果如下

 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

Testing


See also