Go 1.14 中 Cleanup 方法簡介
目錄
- 一般的測試
- 使用
defer
清除依賴 - 使用
Cleanup
- 關於
t.Parallel
- 總結
原文:What's New In Go 1.14: Test Cleanup
單元測試通常遵循某些步驟。首先,建立單元測試的依賴關係;接下來執行測試的邏輯;然後,比較測試結果是否達到我們的期望;最後,清除測試時的依賴關係,為避免影響其他單元測試要將測試環境還原。在Go1.14中,testing 包現在有了 testing.(*T).Cleanup
方法,其目的是更加容易地建立和清除測試的依賴關係。
一般的測試
通常,應用會有某些 類似於儲存庫 的結構,用作對資料庫的訪問。測試這些結構可能有點挑戰性,因為測試時會更改資料庫的資料狀態。通常,測試會有個函式例項化該結構物件:
1. func NewTestTaskStore(t *testing.T) *pg.TaskStore { 2. store := &pg.TaskStore{ 3. Config: pg.Config{ 4. Host: os.Getenv("PG_HOST"), 5. Port: os.Getenv("PG_PORT"), 6. Username: "postgres", 7. Password: "postgres", 8. DBName: "task_test", 9. TLS: false, 10. }, 11. } 12. 13. err = store.Open() 14. if err != nil { 15. t.Fatal("error opening task store: err:", err) 16. } 17. 18. return store 19. }
這為我們提供了一個支援Postgres儲存的新商店例項,該例項負責在任務跟蹤程式中儲存不同的任務。現在我們可以生成此儲存的例項,併為其編寫一個測試:
1. func Test_TaskStore_Count(t *testing.T) { 2. store := NewTestTaskStore(t) 3. 4. ctx := context.Background() 5. _, err := store.Create(ctx, tasks.Task{ 6. Name: "Do Something", 7. }) 8. if err != nil { 9. t.Fatal("error creating task: err:", err) 10. } 11. 12. tasks, err := store.All(ctx) 13. if err != nil { 14. t.Fatal("error fetching all tasks: err:", err) 15. } 16. 17. exp := 1 18. got := len(tasks) 19. 20. if exp != got { 21. t.Error("unexpected task count returned: got:", got, "exp:", exp) 22. } 23. }
該測試的目的是好的——確保在建立一個任務後僅返回一個任務。當執行該測試後它通過了:
$ export PG_HOST=127.0.0.1
$ export PG_PORT=5432
$ go test -count 1 -v ./...
? github.com/timraymond/cleanuptest [no test files]
=== RUN Test_TaskStore_LoadStore
--- PASS: Test_TaskStore_LoadStore (0.01s)
=== RUN Test_TaskStore_Count
--- PASS: Test_TaskStore_Count (0.01s)
PASS
ok github.com/timraymond/cleanuptest/pg 0.035s
因為測試框架將快取測試通過並假定測試會繼續通過,所以必須在這些測試中新增 -count 1
繞過測試快取。當再次允許測試時,測試失敗了:
$ go test -count 1 -v ./...
? github.com/timraymond/cleanuptest [no test files]
=== RUN Test_TaskStore_LoadStore
--- PASS: Test_TaskStore_LoadStore (0.01s)
=== RUN Test_TaskStore_Count
Test_TaskStore_Count: pg_test.go:79: unexpected task count returned: got: 2 exp: 1
--- FAIL: Test_TaskStore_Count (0.01s)
FAIL
FAIL github.com/timraymond/cleanuptest/pg 0.029s
FAIL
使用 defer
清除依賴
測試不會自動清除環境依賴,因此現有狀態會使以後的測試結果無效。最簡單的修復方法是在測試完後使用defer函式清除狀態。由於每個使用 TaskStore 的測試都必須這樣做,因此從例項化 TaskStore 的函式中返回一個清理函式是有意義的:
1. func NewTestTaskStore(t *testing.T) (*pg.TaskStore, func()) {
2. store := &pg.TaskStore{
3. Config: pg.Config{
4. Host: os.Getenv("PG_HOST"),
5. Port: os.Getenv("PG_PORT"),
6. Username: "postgres",
7. Password: "postgres",
8. DBName: "task_test",
9. TLS: false,
10. },
11. }
12.
13. err := store.Open()
14. if err != nil {
15. t.Fatal("error opening task store: err:", err)
16. }
17.
18. return store, func() {
19. if err := store.Reset(); err != nil {
20. t.Error("unable to truncate tasks: err:", err)
21. }
22. }
23. }
在第18-21行,返回一個呼叫 * pg.TaskStore
的 Reset 方法的閉包,該閉包從作為第一個引數返回的中呼叫。在測試中,我們必須確保在defer中呼叫該閉包:
1. func Test_TaskStore_Count(t *testing.T) {
2. store, cleanup := NewTestTaskStore(t)
3. defer cleanup()
4.
5. ctx := context.Background()
6. _, err := store.Create(ctx, tasks.Task{
7. Name: "Do Something",
8. })
9. if err != nil {
10. t.Fatal("error creating task: err:", err)
11. }
12.
13. tasks, err := store.All(ctx)
14. if err != nil {
15. t.Fatal("error fetching all tasks: err:", err)
16. }
17.
18. exp := 1
19. got := len(tasks)
20.
21. if exp != got {
22. t.Error("unexpected task count returned: got:", got, "exp:", exp)
23. }
24. }
現在測試正常了,如果需要更多的defer呼叫,程式碼就會越來越臃腫。如何保證每一個都會執行到?如果某一個defer執行時painc了怎麼辦?這些額外的工作分散了對測試的專注。此外,如果測試必須要考慮這些動態部分,測試會越來越困難。如果想更容易點測試,則需要編寫更多的程式碼。
使用 Cleanup
Go1.14引入了 testing.(* T).Cleanup
方法,可以註冊對測試者透明執行的清理函式。現在用 Cleanup
重構工廠函式:
1. func NewTestTaskStore(t *testing.T) *pg.TaskStore {
2. store := &pg.TaskStore{
3. Config: pg.Config{
4. Host: os.Getenv("PG_HOST"),
5. Port: os.Getenv("PG_PORT"),
6. Username: "postgres",
7. Password: "postgres",
8. DBName: "task_test",
9. TLS: false,
10. },
11. }
12.
13. err = store.Open()
14. if err != nil {
15. t.Fatal("error opening task store: err:", err)
16. }
17.
18. t.Cleanup(func() {
19. if err := store.Reset(); err != nil {
20. t.Error("error resetting:", err)
21. }
22. })
23.
24. return store
25. }
NewTestTaskStore
函式仍然需要 *testing.T
引數,如果不能連線 Postgres 測試會失敗。在18-22行,呼叫 Cleanup
方法,並使用包含store
的Reset
方法的func作為引數。不像 defer 那樣,func
會在每個測試的最後去執行。整合到測試函式:
1. func Test_TaskStore_Count(t *testing.T) {
2. store := NewTestTaskStore(t)
3.
4. ctx := context.Background()
5. _, err := store.Create(ctx, cleanuptest.Task{
6. Name: "Do Something",
7. })
8. if err != nil {
9. t.Fatal("error creating task: err:", err)
10. }
11.
12. tasks, err := store.All(ctx)
13. if err != nil {
14. t.Fatal("error fetching all tasks: err:", err)
15. }
16.
17. exp := 1
18. got := len(tasks)
19.
20. if exp != got {
21. t.Error("unexpected task count returned: got:", got, "exp:", exp)
22. }
23. }
在第2行,只接收了從NewTestTaskStore
返回的 *pg.TaskStore
。很好地封裝了構建*pg.TaskStore
的函式只處理清除依賴和錯誤處理,因此可以僅專注於測試的東西。
關於t.Parallel
使用 testing.(*T).Parallel()
方法能讓測試,子測試在單獨的 Goroutines 中執行。僅需要在測試中呼叫 Parallel()
就能和其他呼叫 Parallel()
的測試一起安全地執行。修改之前的測試開啟多個一樣的子測試:
1. func Test_TaskStore_Count(t *testing.T) {
2. ctx := context.Background()
3. for i := 0; i < 10; i++ {
4. t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
5. t.Parallel()
6. store := NewTestTaskStore(t)
7. _, err := store.Create(ctx, cleanuptest.Task{
8. Name: "Do Something",
9. })
10. if err != nil {
11. t.Fatal("error creating task: err:", err)
12. }
13.
14. tasks, err := store.All(ctx)
15. if err != nil {
16. t.Fatal("error fetching all tasks: err:", err)
17. }
18.
19. exp := 1
20. got := len(tasks)
21.
22. if exp != got {
23. t.Error("unexpected task count returned: got:", got, "exp:", exp)
24. }
25. })
26. }
27. }
使用 t.Run()
方法在 for 迴圈中開啟10個子測試。因為都呼叫了 t.Parallel()
,所有的子測試可以併發執行。把建立store
也放到子測試中,因為 store 中的 t
實際上是子測試的 *testing.T
。再新增些log驗證清除函式是否執行。執行go test
看下結果:
=== CONT Test_TaskStore_Count/3
=== CONT Test_TaskStore_Count/8
=== CONT Test_TaskStore_Count/9
=== CONT Test_TaskStore_Count/2
=== CONT Test_TaskStore_Count/4
=== CONT Test_TaskStore_Count/1
Test_TaskStore_Count/3: pg_test.go:77: unexpected task count returned: got: 3 exp: 1
Test_TaskStore_Count/3: pg_test.go:31: cleanup!
Test_TaskStore_Count/5: pg_test.go:77: unexpected task count returned: got: 4 exp: 1
Test_TaskStore_Count/5: pg_test.go:31: cleanup!
Test_TaskStore_Count/9: pg_test.go:77: unexpected task count returned: got: 4 exp: 1
Test_TaskStore_Count/9: pg_test.go:31: cleanup!
Test_TaskStore_Count/2: pg_test.go:77: unexpected task count returned: got: 4 exp: 1
Test_TaskStore_Count/2: pg_test.go:31: cleanup!
=== CONT Test_TaskStore_Count/7
=== CONT Test_TaskStore_Count/6
Test_TaskStore_Count/8: pg_test.go:77: unexpected task count returned: got: 0 exp: 1
Test_TaskStore_Count/8: pg_test.go:31: cleanup!
像預期的那樣,清除函式在子測試結束時執行了,這是因為使用了子測試的 *testing.T
。然而,測試仍然失敗了,因為一個子測試結果仍然對其他的子測試可見,這是因為沒有使用事務。
然而在並行子測試中 t.Cleanup()
是有用的,在本例中最好使用。在測試中結合使用 Cleanup 函式和事務,可能會有更多成功。
總結
t.Cleanup
的“神奇”行為對於我們在Go中的慣用法似乎太機智了。但我也不希望在生產程式碼中使用這種機制。測試和生產程式碼在很多方面不同,因此放寬一些條件以更容易編寫測試程式碼和更容易閱讀測試內容。就像 t.Fatal
和 t.Error
使處理測試中的錯誤變得微不足道一樣,t.Cleanup
有望使保留清理邏輯變得更加容易,而不會像 defer
那樣使測試混