- Go語言精進之路:從新手到高手的編程思想、方法和技巧(2)
- 白明
- 1780字
- 2022-01-04 17:42:19
41.3 測試固件
無論測試代碼是采用傳統的平鋪模式,還是采用基于測試套件和測試用例的xUnit實踐模式進行組織,都有著對測試固件(test fixture)的需求。
測試固件是指一個人造的、確定性的環境,一個測試用例或一個測試套件(下的一組測試用例)在這個環境中進行測試,其測試結果是可重復的(多次測試運行的結果是相同的)。我們一般使用setUp和tearDown來代表測試固件的創建/設置與拆除/銷毀的動作。
下面是一些使用測試固件的常見場景:
- 將一組已知的特定數據加載到數據庫中,測試結束后清除這些數據;
- 復制一組特定的已知文件,測試結束后清除這些文件;
- 創建偽對象(fake object)或模擬對象(mock object),并為這些對象設定測試時所需的特定數據和期望結果。
在傳統的平鋪模式下,由于每個測試函數都是相互獨立的,因此一旦有對測試固件的需求,我們需要為每個TestXxx測試函數單獨創建和銷毀測試固件。看下面的示例:
// chapter8/sources/classic_testfixture_test.go package demo_test ... func setUp(testName string) func() { fmt.Printf("\tsetUp fixture for %s\n", testName) return func() { fmt.Printf("\ttearDown fixture for %s\n", testName) } } func TestFunc1(t *testing.T) { defer setUp(t.Name())() fmt.Printf("\tExecute test: %s\n", t.Name()) } func TestFunc2(t *testing.T) { defer setUp(t.Name())() fmt.Printf("\tExecute test: %s\n", t.Name()) } func TestFunc3(t *testing.T) { defer setUp(t.Name())() fmt.Printf("\tExecute test: %s\n", t.Name()) }
運行該示例:
$go test -v classic_testfixture_test.go === RUN TestFunc1 setUp fixture for TestFunc1 Execute test: TestFunc1 tearDown fixture for TestFunc1 --- PASS: TestFunc1 (0.00s) === RUN TestFunc2 setUp fixture for TestFunc2 Execute test: TestFunc2 tearDown fixture for TestFunc2 --- PASS: TestFunc2 (0.00s) === RUN TestFunc3 setUp fixture for TestFunc3 Execute test: TestFunc3 tearDown fixture for TestFunc3 --- PASS: TestFunc3 (0.00s) PASS ok command-line-arguments 0.005s
上面的示例在運行每個測試函數TestXxx時,都會先通過setUp函數建立測試固件,并在defer函數中注冊測試固件的銷毀函數,以保證在每個TestXxx執行完畢時為之建立的測試固件會被銷毀,使得各個測試函數之間的測試執行互不干擾。
在Go 1.14版本以前,測試固件的setUp與tearDown一般是這么實現的:
func setUp() func(){ ... return func() { } } func TestXxx(t *testing.T) { defer setUp()() ... }
在setUp中返回匿名函數來實現tearDown的好處是,可以在setUp中利用閉包特性在兩個函數間共享一些變量,避免了包級變量的使用。
Go 1.14版本testing包增加了testing.Cleanup方法,為測試固件的銷毀提供了包級原生的支持:
func setUp() func(){ ... return func() { } } func TestXxx(t *testing.T) { t.Cleanup(setUp()) ... }
有些時候,我們需要將所有測試函數放入一個更大范圍的測試固件環境中執行,這就是包級別測試固件。在Go 1.4版本以前,我們僅能在init函數中創建測試固件,而無法銷毀包級別測試固件。Go 1.4版本引入了TestMain,使得包級別測試固件的創建和銷毀終于有了正式的施展舞臺。看下面的示例:
// chapter8/sources/classic_package_level_testfixture_test.go package demo_test ... func setUp(testName string) func() { fmt.Printf("\tsetUp fixture for %s\n", testName) return func() { fmt.Printf("\ttearDown fixture for %s\n", testName) } } func TestFunc1(t *testing.T) { t.Cleanup(setUp(t.Name())) fmt.Printf("\tExecute test: %s\n", t.Name()) } func TestFunc2(t *testing.T) { t.Cleanup(setUp(t.Name())) fmt.Printf("\tExecute test: %s\n", t.Name()) } func TestFunc3(t *testing.T) { t.Cleanup(setUp(t.Name())) fmt.Printf("\tExecute test: %s\n", t.Name()) } func pkgSetUp(pkgName string) func() { fmt.Printf("package SetUp fixture for %s\n", pkgName) return func() { fmt.Printf("package TearDown fixture for %s\n", pkgName) } } func TestMain(m *testing.M) { defer pkgSetUp("package demo_test")() m.Run() }
運行該示例:
$go test -v classic_package_level_testfixture_test.go package SetUp fixture for package demo_test === RUN TestFunc1 setUp fixture for TestFunc1 Execute test: TestFunc1 tearDown fixture for TestFunc1 --- PASS: TestFunc1 (0.00s) === RUN TestFunc2 setUp fixture for TestFunc2 Execute test: TestFunc2 tearDown fixture for TestFunc2 --- PASS: TestFunc2 (0.00s) === RUN TestFunc3 setUp fixture for TestFunc3 Execute test: TestFunc3 tearDown fixture for TestFunc3 --- PASS: TestFunc3 (0.00s) PASS package TearDown fixture for package demo_test ok command-line-arguments 0.008s
我們看到,在所有測試函數運行之前,包級別測試固件被創建;在所有測試函數運行完畢后,包級別測試固件被銷毀。
可以用圖41-3來總結(帶測試固件的)平鋪模式下的測試執行流。

圖41-3 平鋪模式下的測試執行流
有些時候,一些測試函數所需的測試固件是相同的,在平鋪模式下為每個測試函數都單獨創建/銷毀一次測試固件就顯得有些重復和冗余。在這樣的情況下,我們可以嘗試采用測試套件來減少測試固件的重復創建。來看下面的示例:
// chapter8/sources/xunit_suite_level_testfixture_test.go package demo_test ... func suiteSetUp(suiteName string) func() { fmt.Printf("\tsetUp fixture for suite %s\n", suiteName) return func() { fmt.Printf("\ttearDown fixture for suite %s\n", suiteName) } } func func1TestCase1(t *testing.T) { fmt.Printf("\t\tExecute test: %s\n", t.Name()) } func func1TestCase2(t *testing.T) { fmt.Printf("\t\tExecute test: %s\n", t.Name()) } func func1TestCase3(t *testing.T) { fmt.Printf("\t\tExecute test: %s\n", t.Name()) } func TestFunc1(t *testing.T) { t.Cleanup(suiteSetUp(t.Name())) t.Run("testcase1", func1TestCase1) t.Run("testcase2", func1TestCase2) t.Run("testcase3", func1TestCase3) } func func2TestCase1(t *testing.T) { fmt.Printf("\t\tExecute test: %s\n", t.Name()) } func func2TestCase2(t *testing.T) { fmt.Printf("\t\tExecute test: %s\n", t.Name()) } func func2TestCase3(t *testing.T) { fmt.Printf("\t\tExecute test: %s\n", t.Name()) } func TestFunc2(t *testing.T) { t.Cleanup(suiteSetUp(t.Name())) t.Run("testcase1", func2TestCase1) t.Run("testcase2", func2TestCase2) t.Run("testcase3", func2TestCase3) } func pkgSetUp(pkgName string) func() { fmt.Printf("package SetUp fixture for %s\n", pkgName) return func() { fmt.Printf("package TearDown fixture for %s\n", pkgName) } } func TestMain(m *testing.M) { defer pkgSetUp("package demo_test")() m.Run() }
這個示例采用了xUnit實踐的測試代碼組織方式,將對測試固件需求相同的一組測試用例放在一個測試套件中,這樣就可以針對測試套件創建和銷毀測試固件了。
運行一下該示例:
$go test -v xunit_suite_level_testfixture_test.go package SetUp fixture for package demo_test === RUN TestFunc1 setUp fixture for suite TestFunc1 === RUN TestFunc1/testcase1 Execute test: TestFunc1/testcase1 === RUN TestFunc1/testcase2 Execute test: TestFunc1/testcase2 === RUN TestFunc1/testcase3 Execute test: TestFunc1/testcase3 tearDown fixture for suite TestFunc1 --- PASS: TestFunc1 (0.00s) --- PASS: TestFunc1/testcase1 (0.00s) --- PASS: TestFunc1/testcase2 (0.00s) --- PASS: TestFunc1/testcase3 (0.00s) === RUN TestFunc2 setUp fixture for suite TestFunc2 === RUN TestFunc2/testcase1 Execute test: TestFunc2/testcase1 === RUN TestFunc2/testcase2 Execute test: TestFunc2/testcase2 === RUN TestFunc2/testcase3 Execute test: TestFunc2/testcase3 tearDown fixture for suite TestFunc2 --- PASS: TestFunc2 (0.00s) --- PASS: TestFunc2/testcase1 (0.00s) --- PASS: TestFunc2/testcase2 (0.00s) --- PASS: TestFunc2/testcase3 (0.00s) PASS package TearDown fixture for package demo_test ok command-line-arguments 0.005s
當然在這樣的測試代碼組織方式下,我們仍然可以單獨為每個測試用例創建和銷毀測試固件,從而形成一種多層次的、更靈活的測試固件設置體系。可以用圖41-4總結一下這種模式下的測試執行流。

圖41-4 xUnit實踐模式下的測試執行流
小結
在確定了將測試代碼放入包內測試還是包外測試之后,我們在編寫測試前,還要做好測試包內部測試代碼的組織規劃,建立起適合自己項目規模的測試代碼層次體系。簡單的測試可采用平鋪模式,復雜的測試可借鑒xUnit的最佳實踐,利用subtest建立包、測試套件、測試用例三級的測試代碼組織形式,并利用TestMain和testing.Cleanup方法為各層次的測試代碼建立測試固件。
- PhpStorm Cookbook
- Building a Quadcopter with Arduino
- Linux:Embedded Development
- Mastering Akka
- Serverless Web Applications with React and Firebase
- C++ System Programming Cookbook
- Python大規模機器學習
- Python機器學習與量化投資
- Scrapy網絡爬蟲實戰
- Java 7 Concurrency Cookbook
- 前端架構設計
- JSP大學實用教程
- Unity與C++網絡游戲開發實戰:基于VR、AI與分布式架構
- Web前端開發實戰教程(HTML5+CSS3+JavaScript)(微課版)
- Unity3D高級編程:主程手記