这篇文章主要讲讲单元测试代码的可维护性。不知道你是否写过面条式的单元测试,也就是这样的结构。坦白讲,我写过不少:
func TestFoo(t *testing.T) {
// test get
resp, err := GET(blabalbal)
assert.Nil(err)
...
// test post
resp, err = POST(blabalbal)
assert.Nil(err)
...
// test update
resp, err = PUT(blabalbal)
assert.Nil(err)
...
}
绝大部分童鞋这样写的时候,都是为了方便:方便初始化变量,方便复用。但是一旦当用例代码行数过长,而单测恰好又执行失败, 需要找到具体原因时,就会比较困难,调试时,需要花很多时间定位。
Go社区的测试框架,已经提供了两套比较成熟的解决方案:
我们分别看看这两者。
package package_name
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestSpec(t *testing.T) {
// Only pass t into top-level Convey calls
Convey("Given some integer with a starting value", t, func() {
x := 1
Convey("When the integer is incremented", func() {
x++
Convey("The value should be greater by one", func() {
So(x, ShouldEqual, 2)
})
})
Convey("When the integer is incremented again", func() {
x++
Convey("The value should be greater by one", func() {
So(x, ShouldEqual, 2)
})
})
})
}
如上代码,是可以通过的。GoConvey比较特殊的一点,是它是树状执行的,而不是从上到下执行的。也就是说,它是深度优先遍历执行,
且不共享变量的,在 When the integer is incremented
和 When the integer is incremented again
执行时,x的值都是上层
赋值的1。
以上代码执行顺序为:
Given some integer...
-> When the integer is incremented
-> The value should be....
Given some integer...
-> When the integer is incremented again
-> The value should be....
// Basic imports
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
// Define the suite, and absorb the built-in basic suite
// functionality from testify - including a T() method which
// returns the current testing context
type ExampleTestSuite struct {
suite.Suite
VariableThatShouldStartAtFive int
}
// Make sure that VariableThatShouldStartAtFive is set to five
// before each test
func (suite *ExampleTestSuite) SetupTest() {
suite.VariableThatShouldStartAtFive = 5
}
// All methods that begin with "Test" are run as tests within a
// suite.
func (suite *ExampleTestSuite) TestExample() {
assert.Equal(suite.T(), 5, suite.VariableThatShouldStartAtFive)
suite.Equal(5, suite.VariableThatShouldStartAtFive)
}
// In order for 'go test' to run this suite, we need to create
// a normal test function and pass our suite to suite.Run
func TestExampleTestSuite(t *testing.T) {
suite.Run(t, new(ExampleTestSuite))
}
suite 主要通过如下几个hook函数:
// SetupAllSuite has a SetupSuite method, which will run before the
// tests in the suite are run.
// 执行测试之前先执行这个
type SetupAllSuite interface {
SetupSuite()
}
// SetupTestSuite has a SetupTest method, which will run before each
// test in the suite.
// 执行每个用例之前都会执行这个
type SetupTestSuite interface {
SetupTest()
}
// TearDownAllSuite has a TearDownSuite method, which will run after
// all the tests in the suite have been run.
// 执行整个测试之后,执行这个
type TearDownAllSuite interface {
TearDownSuite()
}
// TearDownTestSuite has a TearDownTest method, which will run after
// each test in the suite.
// 执行每个用例之后都会执行这个
type TearDownTestSuite interface {
TearDownTest()
}
这样,就可以把共享变量以及销毁等分别放置到对应函数进行处理,从而将一系列函数整合成一套一套的测试。
我个人更喜欢用 convey
,只要理解它的树状执行模式,就会发现这样整体测试代码可以少写很多,结构也很清晰。通过树状组织,
可以将同一主题的测试用例,放在同一个 TestXXX
函数中,然后逐层根据条件细化,分别放在各个 Convey
函数中,最后通过
So
传入断言,进行校验。
这篇文章没有讲具体技术的东西,主要是简单介绍了两个单元测试框架,但最重要的,是想要说明单元测试代码,同样是需要受到 重视的代码,也需要好好地组织代码结构和用例,单元测试是用来确保代码本身执行的,通常写好以后,变更频率都不会太高,如果 使用面条式组织方式,在时间久了以后,调试起来非常困难。
借助 convey 这种工具,就可以将测试用例代码细分到不同函数,且互不干扰,非常有利于维护性。