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

推荐订阅源

Cyberwarzone
Cyberwarzone
S
Secure Thoughts
L
LINUX DO - 热门话题
C
Cyber Attacks, Cyber Crime and Cyber Security
C
CERT Recently Published Vulnerability Notes
P
Privacy & Cybersecurity Law Blog
A
Arctic Wolf
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
C
Cybersecurity and Infrastructure Security Agency CISA
大猫的无限游戏
大猫的无限游戏
U
Unit 42
Recorded Future
Recorded Future
Hacker News: Ask HN
Hacker News: Ask HN
F
Full Disclosure
Spread Privacy
Spread Privacy
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Know Your Adversary
Know Your Adversary
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
云风的 BLOG
云风的 BLOG
Stack Overflow Blog
Stack Overflow Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
F
Fortinet All Blogs
Martin Fowler
Martin Fowler
T
Threatpost
I
InfoQ
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Cloudbric
Cloudbric
L
LangChain Blog
N
Netflix TechBlog - Medium
The Register - Security
The Register - Security
S
Security @ Cisco Blogs
B
Blog
T
Threat Research - Cisco Blogs
T
The Blog of Author Tim Ferriss
The GitHub Blog
The GitHub Blog
D
DataBreaches.Net
Simon Willison's Weblog
Simon Willison's Weblog
P
Proofpoint News Feed
Microsoft Security Blog
Microsoft Security Blog
Blog — PlanetScale
Blog — PlanetScale
B
Blog RSS Feed
MongoDB | Blog
MongoDB | Blog
W
WeLiveSecurity
Forbes - Security
Forbes - Security
O
OpenAI News
C
Check Point Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
The Last Watchdog
The Last Watchdog
H
Help Net Security
SecWiki News
SecWiki News

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为例) 读《为什么学生不喜欢上学》 OpenAI Prompt Engineering 摘录和总结 读《打造真正的新产品》 VueJS 总结 Linux 自动挂载 alist 提供的webdav FreeBSD 使用 vm-bhyve 安装Debian虚拟机 FreeBSD 和 Linux 网卡聚合实现提速 GPT 帮我搞定了时区转换问题 长任务系统如何处理? macOS/Linux 编译 InputLeap 使用开源软KVM - synergy-core 解决 macOS 终端hostname一直变化问题 KVM 共享 Intel 集成显卡 PromQL 备忘 读《格鲁夫给经理人的第一课》 读《打开心智》 为什么要把复杂的联表操作拆成多个单表查询? 红包系统的设计 MySQL Index Condition Pushdown Optimization Go mod 简明教程 OpenWRT 使用 Android/iOS USB 网络 搭建旁路由 Golang gRPC 错误处理 OAuth 2 详解(六):Authorization Code Flow with PKCE OAuth 2 详解(五):Device Authorization Flow OAuth 2 详解(三):Resource Owner Password Credentials Grant OAuth 2 详解(四):Client Credentials Flow OAuth 2 详解(二):Implict Grant Flow OAuth 2 详解(一):简介及 Authorization Code 模式 ElasticSearch 学习笔记 三种git流程以及发版模型 错误处理实践 权限模型(RBAC/ABAC) OIDC(OpenID Connect) 简介 任务队列简介 PostgreSQL 操作笔记 使用Drone CI构建CI/CD系统 Golang migrate 做数据库变更管理 使用PostgreSQL做搜索引擎 Nginx 源码阅读(三): 连接池、内存池 Nginx 源码阅读(二): 请求处理 Nginx 源码阅读(一): 启动流程 Go 泛型简明教程 KVM 显卡穿透给 Windows 使用 HTTP Router 处理 Telegram Bot 按钮回调 使用反射(reflect)对结构体赋值 GIN 是如何绑定参数的 你好 2022(2021 年终总结) 用Go导入大型CSV到PostgreSQL 使用 OpenWRT 搭建软路由 使用软KVM切换器 barrier 共享键鼠 SQL 防注入及原理 使用 gomock 测试 Go 代码 gevent不是黑魔法(二): gevent 实现 gevent不是黑魔法(一): greenlet 实现 用 entgo 替代 gorm 应用内使用crontab不是那么方便 单测时要不要 mock 数据库? Sentry 自建指南 用selenium完成自动化任务 用闲置的安卓手机做垃圾电话短信过滤 推荐三个时间管理工具 一次事故反思 当JS遇到uint64:JS整数溢出问题 SQLite3 存储以及ACID原理 Redis源码阅读:pub/sub实现 Redis源码阅读:zset实现 Redis源码阅读:bitmap 位图的运算 Redis源码阅读:set是怎么做交并集运算的? Redis源码阅读:list实现(ziplist, quicklist)
编写可维护的单元测试代码
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 这种工具,就可以将测试用例代码细分到不同函数,且互不干扰,非常有利于维护性。