在使用 testing package 时,如何进行整体测试设置处理,从而为所有测试奠定基础?
作为 Nunit 中的示例,有一个 [SetUp]
属性。
[TestFixture]
public class SuccessTests
{
[SetUp] public void Init()
{ /* Load test data */ }
}
从 Go 1.4 开始,您可以实现 setup/teardown(无需在每次测试之前/之后复制您的函数)。 Main 部分中here概述了该文档:
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) }
其他几个示例 can be found here。
在最新版本中添加到 Go 的测试框架中的 TestMain 功能是针对多个测试用例的简单解决方案。 TestMain 提供了一个全局钩子来执行设置和关闭,控制测试环境,在子进程中运行不同的代码,或者检查测试代码泄漏的资源。大多数软件包不需要 TestMain,但在需要它的时候它是一个受欢迎的补充。
这可以通过在 myfile_test.go
文件中放置一个 init()
函数来实现。这将在 init()
函数之前运行。
// myfile_test.go
package main
func init() {
/* load test data */
}
myfile_test.init() 将在包 init() 函数之前调用。
[TestFixtureSetUp]
属性。
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
%
您可以使用这种方法将一些额外的参数传递给 setup/teardown。
Go 测试框架没有任何与 NUnit 的 SetUp attribute 等效的东西(标记要在套件中的每个测试之前调用的函数)。不过有几个选项:
只需在需要的每个测试中调用您的 SetUp 函数。使用实现 xUnit 范式和概念的 Go 测试框架的扩展。想到三个强有力的选择:gocheck 作证 gunit
这些库中的每一个都鼓励您将测试组织到类似于其他 xUnit 框架的套件/夹具中,并将在每个 Test*
方法之前调用套件/夹具类型的设置方法。
通常,go 中的测试与其他语言的编写风格不同。通常,测试函数相对较少,但每个函数都包含一组表驱动的测试用例。请参阅this article written by one of the Go team.
对于表驱动测试,您只需将任何设置代码放在执行表中指定的各个测试用例的循环之前,然后再放置任何清理代码。
如果您仍然在测试函数之间共享设置代码,则可以将共享设置代码提取到一个函数中,如果重要的是它只执行一次很重要,则可以使用 sync.Once
(或者按照另一个答案的建议,使用 init()
,但这缺点是即使没有运行测试用例也会完成设置(可能是因为您使用 go test -run <regexp>
限制了测试用例。)
我会说,如果您认为您需要在不同测试之间共享设置,这些设置在您确实需要时执行一次,如果您真的需要它,并且如果表驱动测试不会更好。
使用以下模板,您可以在执行设置和拆卸的每个 TestMethod 中进行单行调用。
func setupTest() func() {
// Setup code here
// tear down later
return func() {
// tear-down code here
}
}
func TestMethod(t *testing.T) {
defer setupTest()()
// asserts, ensures, requires... here
}
如果有人正在寻找@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
无耻的插件,我创建了 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{})
}
您可以在一个包中创建任何您想要的测试组,每个测试组使用一组不同的设置/拆卸例程。
您可以将 testing package 用于测试 setup - 将为所有测试设置阶段,而 teardown - 将在测试运行后清理阶段。
下面计算矩形的面积:
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)
}
这是使用 TestMain
作为 Salvador Dali 的 explains 进行测试设置和拆卸的实现。 (注意,从 v1.15 开始,不再需要 TestMain
函数来调用 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----")
})
}
TestMain
曾经在一个包中,所以它不是很有用。我发现 subtests 更适合更复杂的用途。lstat $GOROOT/subtests: no such file or directory