编写可维护的单元测试代码

这篇文章主要讲讲单元测试代码的可维护性。不知道你是否写过面条式的单元测试,也就是这样的结构。坦白讲,我写过不少:

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社区的测试框架,已经提供了两套比较成熟的解决方案:

我们分别看看这两者。

GoConvey

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 incrementedWhen 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....

testify assert suite

// 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 这种工具,就可以将测试用例代码细分到不同函数,且互不干扰,非常有利于维护性。


更多文章
  • Redis源码阅读:RDB是怎么实现的
  • Redis源码阅读:AOF重写
  • Redis源码阅读:AOF持久化
  • Redis源码阅读:key是怎么过期的
  • Redis源码阅读:字典是怎么实现的
  • Redis源码阅读:执行命令
  • Redis源码阅读:启动过程
  • WAL(Write-ahead logging)的套路
  • 搞定CORS问题
  • 如何定位程序问题所在
  • 设计一个IM归档系统
  • logrotate read only filesystem问题
  • Golang GIN写单测时,愉快的使用返回值
  • Python Queue源码分析
  • Go里优雅的使用全局配置