官术网_书友最值得收藏!

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來總結(帶測試固件的)平鋪模式下的測試執行流。

015-01

圖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總結一下這種模式下的測試執行流。

015-01

圖41-4 xUnit實踐模式下的測試執行流

小結

在確定了將測試代碼放入包內測試還是包外測試之后,我們在編寫測試前,還要做好測試包內部測試代碼的組織規劃,建立起適合自己項目規模的測試代碼層次體系。簡單的測試可采用平鋪模式,復雜的測試可借鑒xUnit的最佳實踐,利用subtest建立包、測試套件、測試用例三級的測試代碼組織形式,并利用TestMain和testing.Cleanup方法為各層次的測試代碼建立測試固件。

主站蜘蛛池模板: 葫芦岛市| 长乐市| 泸水县| 桦南县| 贡嘎县| 左权县| 永顺县| 东丽区| 濮阳市| 江门市| 扎囊县| 皋兰县| 天津市| 涡阳县| 江北区| 南木林县| 克山县| 乐清市| 淮南市| 德惠市| 和顺县| 育儿| 高陵县| 尉氏县| 上栗县| 沂南县| 闽侯县| 会东县| 海南省| 光山县| 建平县| 祁连县| 潼南县| 镇江市| 外汇| 自贡市| 大丰市| 尖扎县| 罗源县| 共和县| 宁城县|