惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Blog — PlanetScale
Blog — PlanetScale
J
Java Code Geeks
N
Netflix TechBlog - Medium
Martin Fowler
Martin Fowler
A
About on SuperTechFans
腾讯CDC
B
Blog RSS Feed
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Microsoft Azure Blog
Microsoft Azure Blog
D
Docker
Y
Y Combinator Blog
Microsoft Security Blog
Microsoft Security Blog
F
Fortinet All Blogs
I
InfoQ
博客园 - 【当耐特】
美团技术团队
GbyAI
GbyAI
量子位
宝玉的分享
宝玉的分享
爱范儿
爱范儿
有赞技术团队
有赞技术团队
博客园 - Franky
L
LangChain Blog
阮一峰的网络日志
阮一峰的网络日志

Jiajun的技术笔记

你好,2026! TiDB 源码阅读(六):TiDB Coprocessor 源码解析 性能优化的核心思想 TiDB 源码阅读(五):索引 TiDB 源码阅读(四):AST、逻辑计划、物理计划 CockroachDB Serverless Architecture podman 无故退出 Cursor Control-L (CTRL-L) Keyboard Shortcuts in Terminal Replace docker with podman Using xmonad with xfce4 A RC script for freebsd frpc 自己动手写一个k8s controller AI 会取代你的(编程)岗位吗? 自建DERP服务器提升Tailscale连接速度(使用Nginx转发) 自动升级Docker容器 再读《程序员修炼之道-从小工到专家》 让浏览器下载文件 再读《软件随想录》/《黑客与画家》/《软技能》 HTTP 压力测试中的 Coordinated Omission 2的补码 编程语言中的 context 是什么? flutter macOS 构建出错 Flatpak 使用小记 Golang CAS 操作是怎么实现的 PostgreSQL 当MQ来使用 Clash 结合 工作VPN 的网络设计 使用 PostgreSQL 搭建 JuiceFS PostgreSQL 配置优化和日志分析 有GitHub Copilot?那就可以搭建你的ChatGPT4服务 窗口函数的使用(以PG为例)
编写可维护的单元测试代码
Jiajun Huang · 2022-10-31 · via Jiajun的技术笔记

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

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

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