簡體   English   中英

如何使用 Go 中的測試 package 進行測試設置

[英]How can I do test setup using the testing package in Go

使用測試 package時,如何進行整體測試設置處理,為所有測試設置階段?

作為 Nunit 中的一個例子,有一個[SetUp]屬性。

[TestFixture]
public class SuccessTests
{
  [SetUp] public void Init()
  { /* Load test data */ }
}

從 Go 1.4 開始,您可以實現設置/拆卸(無需在每次測試之前/之后復制您的功能)。 該文檔概括這里主要部分:

TestMain 運行在主 goroutine 中,並且可以圍繞對 m.Run 的調用進行任何必要的設置和拆卸。 然后它應該使用 m.Run 的結果調用 os.Exit

我花了一些時間才弄清楚這意味着如果測試包含一個函數func TestMain(m *testing.M)那么這個函數將被調用而不是運行測試。 在這個函數中,我可以定義測試將如何運行。 例如我可以實現全局設置和拆卸:

 func TestMain(m *testing.M) { setup() code := m.Run() shutdown() os.Exit(code) }

可以在此處找到其他一些示例。

在最新版本中添加到 Go 測試框架的 TestMain 功能是幾個測試用例的簡單解決方案。 TestMain 提供了一個全局鈎子來執行設置和關閉,控制測試環境,在子進程中運行不同的代碼,或者檢查測試代碼泄漏的資源。 大多數包不需要 TestMain,但在需要時它是一個受歡迎的補充。

這可以通過在myfile_test.go文件中放置一個init()函數來實現。 這將在init()函數之前運行。

// myfile_test.go
package main

func init() {
     /* load test data */
}

myfile_test.init() 將在包 init() 函數之前調用。

給定一個簡單的單元測試函數:

package math

func Sum(a, b int) int {
    return a + b
}

您可以使用返回拆卸函數的設置函數對其進行測試。 在調用 setup() 之后,您可以延遲調用 teardown()。

package math

import "testing"

func setupTestCase(t *testing.T) func(t *testing.T) {
    t.Log("setup test case")
    return func(t *testing.T) {
        t.Log("teardown test case")
    }
}

func setupSubTest(t *testing.T) func(t *testing.T) {
    t.Log("setup sub test")
    return func(t *testing.T) {
        t.Log("teardown sub test")
    }
}

func TestAddition(t *testing.T) {
    cases := []struct {
        name     string
        a        int
        b        int
        expected int
    }{
        {"add", 2, 2, 4},
        {"minus", 0, -2, -2},
        {"zero", 0, 0, 0},
    }

    teardownTestCase := setupTestCase(t)
    defer teardownTestCase(t)

    for _, tc := range cases {
        t.Run(tc.name, func(t *testing.T) {
            teardownSubTest := setupSubTest(t)
            defer teardownSubTest(t)

            result := Sum(tc.a, tc.b)
            if result != tc.expected {
                t.Fatalf("expected sum %v, but got %v", tc.expected, result)
            }
        })
    }
}

Go 測試工具將在 shell 控制台中報告日志語句:

% go test -v
=== RUN   TestAddition
=== RUN   TestAddition/add
=== RUN   TestAddition/minus
=== RUN   TestAddition/zero
--- PASS: TestAddition (0.00s)
    math_test.go:6: setup test case
    --- PASS: TestAddition/add (0.00s)
        math_test.go:13: setup sub test
        math_test.go:15: teardown sub test
    --- PASS: TestAddition/minus (0.00s)
        math_test.go:13: setup sub test
        math_test.go:15: teardown sub test
    --- PASS: TestAddition/zero (0.00s)
        math_test.go:13: setup sub test
        math_test.go:15: teardown sub test
    math_test.go:8: teardown test case
PASS
ok      github.com/kare/go-unit-test-setup-teardown 0.010s
% 

您可以使用這種方法將一些額外的參數傳遞給設置/拆卸。

Go 測試框架沒有任何等同於 NUnit 的SetUp 屬性(標記在套件中的每個測試之前要調用的函數)。 不過有幾個選擇:

  1. 只需撥打您SetUp從那里需要每個測試功能。

  2. 使用實現 xUnit 范式和概念的 Go 測試框架的擴展。 我想到了三個強有力的選擇:

這些庫中的每一個都鼓勵您將測試組織到套件/夾具中,類似於其他 xUnit 框架,並將在每個Test*方法之前調用套件/夾具類型上的設置方法。

通常,go 中的測試與其他語言的編寫風格不同。 通常,測試函數相對較少,但每個函數都包含一組表驅動的測試用例。 請參閱Go 團隊之一撰寫的這篇文章。

使用表驅動測試,您只需在執行表中指定的各個測試用例的循環之前放置任何設置代碼,然后放置任何清理代碼。

如果您仍然在測試函數之間共享設置代碼,則可以將共享設置代碼提取到一個函數中,並使用sync.Once如果它只執行一次很重要(或者作為另一個答案建議,使用init() ,但這缺點是即使沒有運行測試用例也會完成設置(可能是因為您使用go test -run <regexp>限制了測試用例。)

我想說的是,如果您認為需要在不同的測試之間共享設置,而這些設置恰好在執行一次后,您應該考慮一下是否真的需要它,以及表驅動的測試是否不會更好。

無恥的插件,我創建了https://github.com/houqp/gtest來幫助解決這個問題。

這是一個快速示例:

import (
  "strings"
  "testing"
  "github.com/houqp/gtest"
)

type SampleTests struct{}

// Setup and Teardown are invoked per test group run
func (s *SampleTests) Setup(t *testing.T)      {}
func (s *SampleTests) Teardown(t *testing.T)   {}
// BeforeEach and AfterEach are invoked per test run
func (s *SampleTests) BeforeEach(t *testing.T) {}
func (s *SampleTests) AfterEach(t *testing.T)  {}

func (s *SampleTests) SubTestCompare(t *testing.T) {
  if 1 != 1 {
    t.FailNow()
  }
}

func (s *SampleTests) SubTestCheckPrefix(t *testing.T) {
  if !strings.HasPrefix("abc", "ab") {
    t.FailNow()
  }
}

func TestSampleTests(t *testing.T) {
  gtest.RunSubTests(t, &SampleTests{})
}

您可以在一個包中創建任何您想要的測試組,每個測試組都使用不同的設置/拆卸例程。

如果有人正在尋找 @BeforeEach(在測試文件中的每個測試之前運行)和 @AfterEach(在測試文件中的測試之后運行)的替代方案,這里有一個幫助程序片段。

func CreateForEach(setUp func(), tearDown func()) func(func()) {
    return func(testFunc func()) {
        setUp()
        testFunc()
        tearDown()
    }
}

您可以在 TestMain 的幫助下像下面一樣使用它

var RunTest = CreateForEach(setUp, tearDown)

func setUp() {
   // SETUP METHOD WHICH IS REQUIRED TO RUN FOR EACH TEST METHOD
   // your code here
}

func tearDown() {
  // TEAR DOWN METHOD WHICH IS REQUIRED TO RUN FOR EACH TEST METHOD
  // your code here
}

fun TestSample(t *testing.T) {
  RunTest(func() {
    // YOUR CODE HERE
  })
}

您也可以檢查: go-beforeeach

您可以使用測試 package進行測試設置——這將為所有測試和拆卸設置階段——這將在測試運行后清理該階段。

下面計算矩形的面積:

package main

import (
    "errors"
    "fmt"
)

func area(height float64, width float64) (float64, error) {
    if height == width {
        fmt.Printf("Rectangle with given dimension is a square. Area is: %f\n", height * width)
        return height * width, nil
    } else if height <= 0 || width <= 0 {
        return 0, errors.New("Both dimensions need to be positive")
    } else {
        fmt.Printf("Area is: %f\n", height * width)
        return height * width, nil
    }
}

func main() {
    var length float64  = 4.0
    var breadth float64 = 5.0
    area(length, breadth)
}

正如 Salvador Dali 所解釋的那樣,這是使用TestMain進行測試設置和拆卸的實現。 請注意,自 v1.15 起,不再需要TestMain function 來調用os.Exit [ ref ])

package main

import (
    "log"
    "testing"
)

var length float64
var breadth float64

func TestMain(m *testing.M) {
    setup()
    m.Run() 
    teardown()
}

func setup() {
    length = 2.0
    breadth = 3.0
    log.Println("\n-----Setup complete-----")
}

func teardown() {
    length = 0
    breadth = 0
    log.Println("\n----Teardown complete----")
}

func TestAreaOfRectangle(t *testing.T) {
    val, err := area(length, breadth)   
    want := 6.0

    if val != want && err != nil {
        t.Errorf("Got %f and %v; Want %f and %v", val, err, want, nil)
    }
}

這是使用子測試進行測試設置和拆卸的實現:

package main

import "testing"

func TestInvalidRectangle(t *testing.T) {
    // setup
    var length float64 = -2.0
    var breadth float64 = 3.0
    t.Log("\n-----Setup complete for invalid rectangle-----")

    // sub-tests
    t.Run("invalid dimensions return value", func(t *testing.T) {
        val, _ := area(length, breadth)
        area := 0.0

        if val != area {
            t.Errorf("Got %f; Want %f", val, area)
        }
    })

    t.Run("invalid dimensions message", func(t *testing.T) {
        _, err := area(length, breadth)
        want := "Both dimensions need to be positive"

        if err.Error() != want {
            t.Errorf("Got error: %v; Want error: %v", err.Error(), want)
        }
    })

    // teardown
    t.Cleanup(func(){
        length = 0
        breadth = 0
        t.Log("\n----Teardown complete for invalid rectangle----")
    })
}

func TestRectangleIsSquare(t *testing.T) {
    var length float64 = 3.0
    var breadth float64 = 3.0
    t.Log("\n-----Rectangle is square setup complete-----")

    t.Run("valid dimensions value and message", func(t *testing.T) {
        val, msg := area(length, breadth)
        area := 9.0
        if val != area && msg != nil {
            t.Errorf("Got %f and %v; Want %f and %v", val, msg, area, nil)
        }
    })

    t.Cleanup(func(){
        length = 0
        breadth = 0
        t.Log("\n----Rectangle is square teardown Complete----")
    })
}

使用以下模板,您可以在每個測試方法中進行一次單行調用,同時進行設置和拆卸。

func setupTest() func() {
    // Setup code here

    // tear down later
    return func() {
        // tear-down code here
    }
}

func TestMethod(t *testing.T) {
    defer setup()()
    // asserts, ensures, requires... here
}

這是運行子測試的最小測試套件框架

  • BeforeAll ,在測試中的所有子測試之前運行
  • BeforeEach ,在每個子測試之前運行
  • AfterEach ,在每個子測試之后運行
  • AfterAll ,在測試中的所有子測試之后運行
package suit

import "testing"

func Of(subTests *SubTests) *SubTests {
    if subTests.AfterAll != nil {
        subTests.T.Cleanup(subTests.AfterAll)
    }
    return subTests
}

type SubTests struct {
    T          *testing.T
    BeforeEach func()
    AfterEach  func()
    AfterAll   func()
}

func (s *SubTests) TestIt(name string, f func(t *testing.T)) {
    if s.AfterEach != nil {
        defer s.AfterEach()
    }
    if s.BeforeEach != nil {
        s.BeforeEach()
    }
    s.T.Run(name, f)
}

用法

func TestFoo(t *testing.T) {
    // BeforeAll setup goes here

    s := suit.Of(&suit.SubTests{
        T:          t,
        BeforeEach: func() { ... },
        AfterEach:  func() { ... },
        AfterAll:   func() { ... },
    })

    s.TestIt("returns true", func(t *testing.T) {
        assert.Equal(t, 1, 1)
    })
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM