Go语言中测试固件
测试固件(test fixture)一般指的是一个人造的、确定性的环境。一个测试用例或一个测试套件在这个环境中运行,其测试结果是可重复的。一般用setUp和tearDown来代表测试固件的创建/设置与拆除/销毁动作。
一般的做法是在setUp中返回匿名函数来实现tearDown。这样做的好处是可以在setUp中利用闭包特性在两个函数间共享一些变量,避免了包级别变量的使用。
func setUp(testName string) func() { ... // 创建测试固件 return func() { // 测试固件销毁函数 ... // 销毁测试固件 } }
来看看具体使用方法:
import "testing" func setUp(testName string) func() { println("setUp fixture") return func() { println("tearDown fixture") } } func TestFunc1(t *testing.T) { defer setUp(t.Name())() println("testing func1") } func TestFunc2(t *testing.T) { defer setUp(t.Name())() println("testing func2") } $ go test -v . === RUN TestFunc1 setUp fixture testing func1 tearDown fixture --- PASS: TestFunc1 (0.00s) === RUN TestFunc2 setUp fixture testing func2 tearDown fixture --- PASS: TestFunc2 (0.00s) PASS ok github.com/demo/test-demo 0.581s
上例中运行每个测试函数TestXxx时,都会先通过setUp函数创建测试固件,并在defer函数中注册测试固件销毁的函数(defer关键字后表达式求值顺序:https://www.nowcoder.com/discuss/468918522509336576),以保证每个TestXxx执行完毕后为其建立的测试固件被销毁,使得各个测试函数之间的测试执行互不干扰。
在Go 1.14版本testing包增加了testing.Cleanup方法,为测试固件的销毁提供了包级原生支持。
func setUp(testName string) func() { println("setUp fixture") return func() { println("tearDown fixture") } } func TestFunc3(t *testing.T) { t.Cleanup(setUp(t.Name())) println("testing func3") } $ go test -v . === RUN TestFunc3 setUp fixture testing func3 tearDown fixture --- PASS: TestFunc3 (0.00s) PASS ok github.com/demo/test-demo 0.610s
有些时候需要将所有测试函数放入一个更大范围的测试固件环境中执行——包级别测试固件。Go 1.14版本中引入了TestMain,使得包级别测试固件的创建和销毁更容易:
import ( "fmt" "testing" ) func setUp(testName string) func() { println("setUp fixture") return func() { println("tearDown fixture") } } func TestFunc1(t *testing.T) { t.Cleanup(setUp(t.Name())) fmt.Printf("testing %s\n", t.Name()) } func TestFunc2(t *testing.T) { t.Cleanup(setUp(t.Name())) fmt.Printf("testing %s\n", t.Name()) } func TestFunc3(t *testing.T) { t.Cleanup(setUp(t.Name())) fmt.Printf("testing %s\n", t.Name()) } func pkgSetUp(testName string) func() { println("package setUp fixture") return func() { println("package tearDown fixture") } } func TestMain(m *testing.M) { defer pkgSetUp("package test") m.Run() } $ go test -v . package setUp fixture === RUN TestFunc1 setUp fixture testing TestFunc1 tearDown fixture --- PASS: TestFunc1 (0.00s) === RUN TestFunc2 setUp fixture testing TestFunc2 tearDown fixture --- PASS: TestFunc2 (0.00s) === RUN TestFunc3 setUp fixture testing TestFunc3 tearDown fixture --- PASS: TestFunc3 (0.00s) PASS package tearDown fixture ok github.com/demo/test-demo 0.596s
从上例中可以看到,包级别测试固件在所有测试函数执行之前就被创建了,在包级别测试函数执行完成之后被销毁。注意:对于包级别测试固件的销毁,没有Cleanup方法,我们依然利用defer的特性实现测试固件的销毁。
如果一些测试函数所需要的测试固件是相同的,可以使用测试套件的方式来减少测试固件的重复创建:
import ( "fmt" "testing" ) func suiteSetUp(testName string) func() { fmt.Printf("setUp fixture for %s\n", testName) return func() { fmt.Printf("tearDown fixture for %s\n", testName) } } func func1TestCase1(t *testing.T) { fmt.Printf("testing %s\n", t.Name()) } func func1TestCase2(t *testing.T) { fmt.Printf("test %s\n", t.Name()) } func func2TestCase1(t *testing.T) { fmt.Printf("testing %s\n", t.Name()) } func func2TestCase2(t *testing.T) { fmt.Printf("testing %s\n", t.Name()) } func TestFunc1(t *testing.T) { t.Cleanup(suiteSetUp(t.Name())) t.Run("testcase1", func1TestCase1) t.Run("testcase2", func1TestCase2) } func TestFunc2(t *testing.T) { t.Cleanup(suiteSetUp(t.Name())) t.Run("testcase1", func2TestCase1) t.Run("testcase1", func2TestCase2) } func pkgSetUp(pkgName string) func() { fmt.Printf("setUp package fixture for %s\n", pkgName) return func() { fmt.Printf("teatDown package fixture for %s\n", pkgName) } } func TestMain(m *testing.M) { defer pkgSetUp("package test")() m.Run() } $ go test -v . setUp package fixture for package test === RUN TestFunc1 setUp fixture for TestFunc1 === RUN TestFunc1/testcase1 testing TestFunc1/testcase1 === RUN TestFunc1/testcase2 test TestFunc1/testcase2 tearDown fixture for TestFunc1 --- PASS: TestFunc1 (0.00s) --- PASS: TestFunc1/testcase1 (0.00s) --- PASS: TestFunc1/testcase2 (0.00s) === RUN TestFunc2 setUp fixture for TestFunc2 === RUN TestFunc2/testcase1 testing TestFunc2/testcase1 === RUN TestFunc2/testcase1#01 testing TestFunc2/testcase1#01 tearDown fixture for TestFunc2 --- PASS: TestFunc2 (0.00s) --- PASS: TestFunc2/testcase1 (0.00s) --- PASS: TestFunc2/testcase1#01 (0.00s) PASS teatDown package fixture for package test ok github.com/demo/test-demo 0.601s