深入掌握 Go 单元测试:从基础到进阶的完整指南

你好,我是陈明勇,一名热爱技术、乐于分享的开发者,同时也是开源爱好者。

成功的路上并不拥挤,有没有兴趣结个伴?

个人网站:https://chenmingyong.cn

文章持续更新,如果本文能让您有所收获,欢迎点赞收藏加关注本号。 微信阅读可搜《程序员陈明勇》。 这篇文章已被收录于 GitHub https://github.com/chenmingyong0423/blog,欢迎大 家Star 催更并持续关注。

前言

在软件开发中,单元测试是一项不可忽视的环节。它不仅帮助开发者在编码的早期阶段发现并解决潜在问题,还能确保代码的可靠性、可维护性和整体质量,这对于提高开发效率、减少后期维护成本非常重要。

尤其是当你在后期对某个函数或方法进行优化时,之前编写的测试用例就显得非常重要。如果测试通过,你会感到欣慰,说明优化后的代码没有破坏现有功能;如果测试失败,那也是好事,因为你及时发现了潜在问题,避免了线上故障的风险。

Go 语言中,go test 命令和 testing 包提供了简洁而强大的测试机制,使得 Gopher 能轻松编写并执行测试用例。本文将详细介绍如何使用 Go 语言中的 testing 包编写高效的单元测试,探讨 go test 命令的常用参数及其作用,并通过子测试和表格驱动测试的实践方法提升代码质量。文章还会介绍 TestMain 函数的使用场景,外部测试工具库如 testify 的应用,以及常用的断言方法。

准备好了吗?准备一杯你最喜欢的咖啡或茶,随着本文一探究竟吧。

程序员陈明勇.jpeg

基本的测试结构

  • Go 语言的测试文件通常放置在与被测试的源文件相同的包中,文件名以 _test.go 结尾。比如,reverse.go 文件的测试文件应命名为 reverse_test.go。这样 go test 命令将能够正确识别和执行测试。

  • 每个测试函数的命名必须以 Test 开头,后接大写字母开头的函数名。测试函数的签名为 func (t *testing.T),其中 t *testing.T 是用于管理测试状态和报告测试失败的参数。

├── stringx/
│   ├── reverse.go
│   └── reverse_test.go

简单案例

reverse.go 里:

package stringxfunc Reverse(s string) string {r := []rune(s)for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {r[i], r[j] = r[j], r[i]}return string(r)
}

reverse_test.go 里:

package stringximport ("testing"
)func TestReverse(t *testing.T) {got := Reverse("陈明勇")if got != "勇明陈" {t.Errorf("expected 勇明陈, but got %s", got)}
}

Reverse 返回的结果是非预期结果时,使用 t.Errorf 方法报告测试失败,并打印相关的参数信息。

stringx 目录下执行 go test 命令:

$ go test
PASS
ok      test_example/stringx    0.166s

go test 常用的参数及其说明

  • -v
    • 作用:显示详细的测试输出,包括每个测试用例的执行情况(测试函数的名字和通过/失败的状态)。
    • 示例:go test -v
$ go test -v       
=== RUN   TestReverse
--- PASS: TestReverse (0.00s)
PASS
ok      test_example/stringx    0.284s
  • -cover
    • 作用:运行测试并显示代码覆盖率的简要统计信息。
    • 示例:go test -cover
$ go test -cover
PASS
coverage: 100.0% of statements
ok      test_example/stringx    0.174s
  • -run <regex>

    • 作用:只运行匹配指定正则表达式的测试函数。
    • 示例:go test -run ^TestFunction$ 只运行 TestFunction
  • -bench <regex>

    • 作用:只运行匹配正则表达式的基准测试(函数名通常以 Benchmark 开头)。
    • 示例:go test -bench . 运行所有基准测试。
  • -benchmem

    • 作用:在运行基准测试时,报告内存分配统计信息。
    • 示例:go test -bench . -benchmem
  • -coverprofile=<filename>

    • 作用:生成代码覆盖率的详细报告并保存到指定的文件中。
    • 示例:go test -coverprofile=coverage.out
  • -covermode=<mode>

    • 作用:指定覆盖模式,有三种模式:
      • set: 统计哪些语句被执行(默认)。
      • count: 统计每个语句被执行的次数。
      • atomic: 统计语句执行次数,并确保多线程安全。
    • 示例:go test -covermode=count
  • -timeout=<duration>

    • 作用:设置测试运行的超时时间,防止测试长时间挂起,默认超时时间为 10 分钟。
    • 示例:go test -timeout=30s
  • -short

    • 作用:告诉测试程序跳过较长的测试。常用于缩短测试时间。
    • 示例:go test -short
  • -parallel=<n>

    • 作用:设置并行执行测试的最大 Goroutine 数量。
    • 示例:go test -parallel=4
  • -race

    • 作用:开启数据竞争检测,适用于并发程序的测试。
    • 示例:go test -race
  • -count=<n>

    • 作用:指定测试的重复运行次数,通常用于检测偶发性错误。
    • 示例:go test -count=3
  • -json

    • 作用:输出测试结果为 JSON 格式,适用于与 CI 系统集成或日志分析。
    • 示例:go test -json
  • -failfast

    • 作用:在测试失败时立即停止执行剩余的测试。
    • 示例:go test -failfast

常用组合命令:

  • 代码覆盖率分析并生成 HTML 报告
go test -coverprofile=coverage.out && go tool cover -html=coverage.out
  • 运行所有测试并输出详细信息
go test -v ./...

这些参数可以根据测试需求灵活组合使用,有助于提高测试的覆盖率、性能分析以及调试能力。

更多的参数可通过运行 go help test 命令进行查看。

子测试的表格驱动测试

表格驱动测试(Table-driven tests)是 Go 语言中常见的测试模式,它通过将多个测试用例组织在一个表格(通常是一个切片)中,使用循环依次执行每个测试用例,从而提高代码的可读性和可维护性。

package stringximport ("testing"
)func TestReverse(t *testing.T) {testCases := []struct {name     stringinput    stringexpected string}{{"empty string", "", ""},                     // 测试空字符串{"reverse Chinese characters", "陈明勇", "勇明陈"}, // 测试中文字符{"reverse English word", "Hello", "olleH"},   // 测试英文单词}for _, tc := range testCases {t.Run(tc.name, func(t *testing.T) {got := Reverse(tc.input)if got != tc.expected {t.Errorf("expected %s, but got %s", tc.expected, got)}})}
}

代码解释:

  • 表格testCases 是一个切片,包含多个结构体,每个结构体代表一个测试用例。
  • 循环测试:通过 for _, tc := range testCases 循环每个测试用例。
  • 子测试:通过 t.Run(tc.name, ...) 方法为每个测试用例创建子测试,这样在运行测试时,可以在控制台输出的信息中看到每个子测试的名称和结果,方便调试和排查问题。

基于表格驱动测试的好处

  • 减少代码的重复性: 避免为每个测试用例单独编写一个测试函数。所有测试用例的核心测试逻辑都可以复用,从而减少代码的冗余。
  • 提高测试代码的可维护性: 如果需要添加新的测试用例,只需在表格(切片)中添加新的数据行,而不需要修改核心测试逻辑。
  • 提高代码的可读性: 测试用例和核心测试逻辑的分离,使测试代码更加简洁、易于理解。

TestMain 函数

TestMain 在测试模块里是一个特殊的函数,用于在执行测试之前或之后执行全局的初始化和清理工作,它是整个测试包的入口点。在运行 go test 命令之后,首先会检查包测试文件里是否定义了 TestMain 函数,如果有,则会调用它来执行测试。如果没有 TestMain 函数,则会默认调用所有的 TextXxx 函数。

TestMain 的函数签名如下所示:

TestMain(m *testing.M)

TestMain 函数通常结合 setupteardown 函数一起使用,前者用于在测试执行之前做一些准备工作(例如连接数据库,初始化配置等),后者用于在测试执行之后做一些清理工作(例如关闭数据库的链接,删除临时文件等)。

下面是代码示例:

package stringximport ("fmt""os""testing"
)func TestReverse(t *testing.T) {testCases := []struct {name     stringinput    stringexpected string}{{"empty string", "", ""},                     // 测试空字符串{"reverse Chinese characters", "陈明勇", "勇明陈"}, // 测试中文字符{"reverse English word", "Hello", "olleH"},   // 测试英文单词}for _, tc := range testCases {t.Run(tc.name, func(t *testing.T) {got := Reverse(tc.input)if got != tc.expected {t.Errorf("expected %s, but got %s", tc.expected, got)}})}
}func setup() {fmt.Println("Before running tests")
}func teardown() {fmt.Println("After running tests")
}func TestMain(m *testing.M) {setup()code := m.Run()teardown()os.Exit(code)
}

关键代码解释:

  • m.Run():通过该方法执行所有的测试函数。它返回一个整数,表示测试的状态码,通常为 0 表示成功,非 0 表示有失败的测试。
  • os.Exit(code) :返回测试结果,确保正确的退出状态。

外部测试工具库

在前面的代码示例中,我们使用 != 运算符来比较 结果预期值 是否不相等,这对于基本数据类型是可行的。然而,当我们需要比较像切片、map 等复杂数据结构时,直接使用 != 就不再适用,必须编写额外的逻辑来进行比较。为了解决这个问题,我们可以借助第三方库,例如 testify,来简化这些比较操作。

testify 工具库

testify 是在 Go 语言中被广泛使用的第三方测试库,它提供了一些便捷的断言方法、测试套件支持和 mock 功能,极大地简化了测试代码的编写。相比 Go 自带的 testing 库,testify 提供了更丰富的函数来进行断言判断,特别是在处理复杂数据结构时更加方便。

我们可以通过以下命令安装 testify 模块:

go get github.com/stretchr/testify

接下来我们就可以将前面展示的部分代码:

if got != tc.expected {t.Errorf("expected %s, but got %s", tc.expected, got)
}

改写成:

assert.Equalf(t, tc.expected, got, "expected %s, but got %s", tc.expected, got)

当断言失败时,会打印出后面的信息。

testify 常用的断言方法

testify/assert 提供了丰富的断言函数,便于我们进行复杂的比较操作。以下是一些常用的断言函数:

  • assert.Equal
    断言两个值相等,适用于基本类型、结构体等。
assert.Equal(t, "勇明陈", Reverse("陈明勇"))  // Reverse("陈明勇" 是否等于 "勇明陈"
  • assert.NotNil
    断言对象不为 nil
var obj = &struct{}{}
assert.NotNil(t, obj)
  • assert.True
    断言条件为 true
var b bool
assert.True(t, b)
  • assert.False
    断言条件为 false
var b bool
assert.False(t, b)
  • assert.ElementsMatch
    用于比较两个切片是否包含相同的元素,无论元素的顺序如何。
expected := []int{1, 2, 3, 4}
actual := []int{4, 3, 2, 1}
assert.ElementsMatch(t, expected, actual)  
  • assert.Len
    断言集合(如切片、map 等)的长度是否等于指定值。
assert.Len(t, []int{1, 2, 3}, 3)

更多的函数信息,请参考 testify/assert。

除了 assert 包,testify 库还提供了另一个 require 包,它与 assert 包的功能类似,都是用于断言的。二者的主要区别在于测试失败时的处理方式:

  • 当断言失败时,assert 包会记录失败信息,但测试会继续执行后续的代码。
assert.Equal(t, "陈明勇", Reverse("陈明勇")) // 失败时记录失败,但继续执行后面的代码
assert.Equal(t, "勇明陈", Reverse("陈明勇")) // 这个断言仍会执行
  • 当断言失败时,require 包会立即停止当前测试的执行,并输出错误信息。测试不会继续执行后续的代码。
require.Equal(t, "陈明勇", Reverse("陈明勇")) // 失败时立即停止执行后续代码
require.Equal(t, "勇明陈", Reverse("陈明勇")) // 如果前一个断言失败,这个不会被执行

我们可以根据具体测试场景选择合适的包,比如在一些关键步骤需要确保不通过就终止测试时使用 require,而对于不那么关键的步骤可以使用 assert,以便测试能继续执行并获得更多结果。

小结

通过本文的介绍,相信你已经掌握了如何在 Go 语言中编写高效的单元测试。从基本的测试结构到表格驱动测试,再到使用外部库 testify 进行更加灵活的断言操作,以及对 go test 命令及其常用参数的掌握。

单元测试不仅是提高代码质量的关键环节,也是保障项目长期稳定的重要实践。无论是个人项目还是大型团队开发,都应该重视测试在整个开发流程中的重要性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/145826.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

罗马数字详解

一. 罗马数字の背景 1. 罗马数字的诞生与进化 罗马数字起源于古罗马帝国&#xff0c;拥有一个漫长而复杂的历史&#xff0c;始于公元前 8 世纪至 9 世纪&#xff0c;与古罗马帝国在帕兰丁山&#xff08;Palantine Hill&#xff09;周围建立的时间大致相同。不过&#xff0c;罗…

铲屎官进!宠物空气净化器真的有用吗?哪款去浮毛效果好

国庆小长假就要来了&#xff0c;别人都在苦恼抢票问题&#xff0c;而我在想会不会被我妈赶出家门... 毕业后我就留在了广州上班&#xff0c;独自一人租房难免会感觉孤独&#xff0c;就养了一只小猫和我作伴。这次放假这么久&#xff0c;我不放心留它一个人在家&#xff0c;也没…

SpringBoot 项目如何使用 pageHelper 做分页处理 (含两种依赖方式)

分页是常见大型项目都需要的一个功能&#xff0c;PageHelper是一个非常流行的MyBatis分页插件&#xff0c;它支持多数据库分页&#xff0c;无需修改SQL语句即可实现分页功能。 本文在最后展示了两种依赖验证的结果。 文章目录 一、第一种依赖方式二、第二种依赖方式三、创建数…

Virtuoso服务在centos中自动停止的原因分析及解决方案

目录 前言1. 问题背景2. 原因分析2.1 终端关闭导致信号12.2 nohup命令的局限性 3. 解决方案3.1 使用 screen 命令保持会话3.2 使用 tmux 作为替代方案3.3 使用系统服务&#xff08;systemd&#xff09; 4. 其他注意事项4.1 网络配置4.2 日志监控 结语 前言 在使用Virtuoso作为…

Transformer 的可视化解释

Transformer 的可视化解释&#xff1a;了解 LLM Transformer 模型如何与交互式可视化配合使用 部署 Nodejs version > 20.0 git clone https://github.com/poloclub/transformer-explainer.git cd transformer-explainer npm install npm run dev# fix: cnpm install --pl…

AD9854 为什么输出波形幅度受限??

&#x1f3c6;本文收录于《全栈Bug调优(实战版)》专栏&#xff0c;主要记录项目实战过程中所遇到的Bug或因后果及提供真实有效的解决方案&#xff0c;希望能够助你一臂之力&#xff0c;帮你早日登顶实现财富自由&#x1f680;&#xff1b;同时&#xff0c;欢迎大家关注&&am…

lambda 自调用递归

从前序与中序遍历序列构造二叉树 官方解析实在是记不住&#xff0c;翻别人的题解发现了一个有意思的写法 class Solution { public:TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {auto dfs [](auto&& dfs, auto&&…

Pandas和matplotlib实现同期天气温度对比

目录 1、下载近两年的天气Excel数据 2、pandas加载Excel 3、将时间作为索引 4、按日计算最值、均值 5、选取近两年同期温度数据 6、同期温度曲线对比,共享y轴 1、下载近两年的天气Excel数据 一个免费的天气数据下载网址:METAR北京(机场)历史天气 (rp5.ru) 选择”北京天…

centos 7.9安装k8s

前言 Kubernetes单词来自于希腊语&#xff0c;含义是领航员&#xff0c;生产环境级别的容器编排技术&#xff0c;可实现容器的自动部署扩容以及管理。Kubernetes也称为K8S&#xff0c;其中8代表中间8个字符&#xff0c;是Google在2014年的开源的一个容器编排引擎技术&#xff…

一文读懂SpringCLoud

一、前言 只有光头才能变强 认识我的朋友可能都知道我这阵子去实习啦&#xff0c;去的公司说是用SpringCloud(但我觉得使用的力度并不大啊~~)… 所以&#xff0c;这篇主要来讲讲SpringCloud的一些基础的知识。(我就是现学现卖了&#xff0c;主要当做我学习SpringCloud的笔记吧&…

【JPCS出版】第二届应用统计、建模与先进算法国际学术会议(ASMA2024,9月27日-29)

第二届应用统计、建模与先进算法国际学术会议 2024 2nd International Conference on Applied Statistics, Modeling and Advanced Algorithms&#xff08;ASMA2024&#xff09; 会议官方 会议官网&#xff1a;www.icasma.org 2024 2nd International Conference on Applied …

Moveit2与gazebo联合仿真:添加摄像头传感器

1.代码更新修改 1.1 添加物理关节 如图&#xff0c;在原有机械臂的基础上添加camera_link和base_camera_joint作为传感器的几何属性 对应的xml代码如下 <link name"${prefix}camera_link"><collision><geometry><box size"0.01 0.1 0.05&…

【Python】练习:控制语句(二)第4关

第4关&#xff1a;控制结构综合实训 第一题第二题&#xff08;※&#xff09;第三题&#xff08;※&#xff09;第四题&#xff08;※&#xff09;第五题&#xff08;※&#xff09;第六题&#xff08;※&#xff09; 第一题 #第一题def rankHurricane(velocity):#请在下面编写…

毫米波雷达预警功能 —— 盲区检测(BSD)预警

文档声明&#xff1a; 以下资料均属于本人在学习过程中产出的学习笔记&#xff0c;如果错误或者遗漏之处&#xff0c;请多多指正。并且该文档在后期会随着学习的深入不断补充完善。感谢各位的参考查看。 笔记资料仅供学习交流使用&#xff0c;转载请标明出处&#xff0c;谢谢配…

MySQL高阶1875-将工资相同的雇员分组

目录 题目 准备数据 分析数据 题目 编写一个解决方案来获取每一个被分配到组中的雇员的 team_id 。 返回的结果表按照 team_id 升序排列。如果相同&#xff0c;则按照 employee_id 升序排列。 这家公司想要将 工资相同 的雇员划分到同一个组中。每个组需要满足如下要求&a…

Lichee NanoKVM基本使用环境

Lichee NanoKVM基本使用环境 本文章主要记录一些自己在初期的使用&#xff0c;以及自己的一些经验 &#xff0c;非常感谢sipeed NanoKVM官方使用教程 外观&#xff08;博主自己的是lite版本&#xff0c;非常感谢sipeed&#xff09; Lichee NanoKVM 是基于 LicheeRV Nano 的 I…

msvcp120dll丢失问题的相关分享,4种靠谱的修复msvcp120dll的方法

在你启动某个软件或游戏的过程中&#xff0c;如果屏幕上突然出现一条提示说“msvcp120.dll文件缺失”这时候请不要紧张&#xff0c;要解决这个问题还是比较简单的。msvcp120.dll 是一个关键的系统文件&#xff0c;属于 Microsoft Visual C 可再发行组件包的一部分。它包含了许多…

电影《祝你幸福!》观后感

上周看了电影《祝你幸福&#xff01;》&#xff0c;虽然讲述的是一个悲伤的故事&#xff0c;但自己看来&#xff0c;其实更是一个人遭遇创伤后&#xff0c;如何自己走出来的过程&#xff0c;尤其重大精神创伤。另外作为本部电影的主角&#xff0c;另一个身份是律师&#xff0c;…

编译成功!QT/6.7.2/Creator编译Windows64 MySQL驱动(MSVC版)

相邻你找了很多博文&#xff0c;都没有办法。现在终于找到了正宗。 参考 GitHub - thecodemonkey86/qt_mysql_driver: Typical symptom: QMYSQL driver not loaded. Solution: get pre-built Qt SQL driver plug-in required to establish a connection to MySQL / MariaDB u…

小红书本地生活,要生活还是生意?

8月&#xff0c;沉寂许久的小红书本地生活突然动作频频。8月23日&#xff0c;小红书新增本地生活服务商管理规范和入驻规则&#xff0c;10天后正式宣布开放全国49座城市的餐饮团购类目&#xff0c;并将技术服务费从0.6%最新调整至2.6%&#xff0c;49城餐饮商家自此打通门店团购…