Go 1.14 中 Cleanup 方法簡介


原文: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 方法,並使用包含storeReset方法的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.Fatalt.Error 使處理測試中的錯誤變得微不足道一樣,t.Cleanup 有望使保留清理邏輯變得更加容易,而不會像 defer 那樣使測試混亂。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM