第 22 章 - Go语言 测试与基准测试
在Go语言中,测试是一个非常重要的部分,它帮助开发者确保代码的正确性、性能以及可维护性。Go语言提供了一套标准的测试工具,这些工具可以帮助开发者编写单元测试、表达式测试(通常也是指单元测试中的断言)、基准测试等。
单元测试 (Unit Testing)
单元测试主要用于验证程序中最小可测试单元的正确性,如一个函数或方法。Go语言使用testing
包来支持单元测试。
示例
假设我们有一个简单的加法函数Add
,我们想要为这个函数编写单元测试。
源代码: add.go
package mainimport "fmt"// Add two integers and return the result.
func Add(a int, b int) int {return a + b
}func main() {fmt.Println(Add(1, 2))
}
测试代码: add_test.go
package mainimport ("testing"
)// TestAdd checks if the Add function works as expected.
func TestAdd(t *testing.T) {tests := []struct {a, b, want int}{{1, 2, 3},{0, 0, 0},{-1, 1, 0},{5, -3, 2},}for _, tt := range tests {testname := fmt.Sprintf("%d+%d", tt.a, tt.b)t.Run(testname, func(t *testing.T) {ans := Add(tt.a, tt.b)if ans != tt.want {t.Errorf("got %d, want %d", ans, tt.want)}})}
}
在这个例子中,我们定义了一个测试表tests
,其中包含了一些输入值和预期结果。对于每个测试用例,我们调用Add
函数并检查返回的结果是否符合预期。如果不符合,我们使用t.Errorf
报告错误。
表达式测试 (Expression Testing)
表达式测试通常是指在单元测试中使用断言来简化测试逻辑。虽然Go语言的标准库没有直接提供断言功能,但是可以通过第三方库如testify/assert
来实现更简洁的测试代码。
使用 testify 的测试代码: add_test.go
package mainimport ("testing""github.com/stretchr/testify/assert"
)func TestAddWithAssert(t *testing.T) {assert.Equal(t, 3, Add(1, 2), "1+2 should equal 3")assert.Equal(t, 0, Add(0, 0), "0+0 should equal 0")assert.Equal(t, 0, Add(-1, 1), "-1+1 should equal 0")assert.Equal(t, 2, Add(5, -3), "5-3 should equal 2")
}
这里使用了testify/assert
库的assert.Equal
函数来比较期望值和实际值,并且提供了失败时的错误信息。
基准测试 (Benchmark Testing)
基准测试用于测量代码的性能。Go语言的testing
包也支持基准测试。
基准测试代码: add_benchmark_test.go
package mainimport "testing"// BenchmarkAdd measures the performance of the Add function.
func BenchmarkAdd(b *testing.B) {for i := 0; i < b.N; i++ {Add(1, 2)}
}
在这个基准测试中,我们通过循环调用Add
函数来测量其执行速度。b.N
是测试框架自动设置的一个变量,表示应该执行的次数。运行基准测试可以使用命令go test -bench=.
。
以上就是Go语言中单元测试、表达式测试和基准测试的基本介绍和示例。希望这些内容能帮助您更好地理解和使用Go语言的测试功能。
接下来,我们将进一步深入探讨一些高级特性和最佳实践,包括子测试、表格驱动测试、并发测试、测试覆盖率以及如何组织测试代码。
子测试 (Subtests)
子测试允许在一个测试函数内部组织多个相关的测试用例。这有助于提高测试的可读性和组织性,特别是在处理复杂的测试场景时。
改进后的测试代码: add_test.go
package mainimport ("testing"
)func TestAddSubtests(t *testing.T) {cases := []struct {name stringa, b, want int}{{"positive", 1, 2, 3},{"zero", 0, 0, 0},{"negative", -1, 1, 0},{"mixed", 5, -3, 2},}for _, c := range cases {t.Run(c.name, func(t *testing.T) {got := Add(c.a, c.b)if got != c.want {t.Errorf("Add(%d, %d) = %d; want %d", c.a, c.b, got, c.want)}})}
}
在这个例子中,我们使用t.Run
来创建子测试,每个子测试都有一个名称,这样在测试失败时可以更容易地定位到具体的问题。
并发测试 (Concurrent Testing)
Go语言的并发模型非常适合编写高效的测试代码。使用testing.T
的Parallel
方法可以在多个CPU核心上并行运行测试,从而加快测试速度。
并发测试代码: add_parallel_test.go
package mainimport ("testing"
)func TestAddParallel(t *testing.T) {cases := []struct {a, b, want int}{{1, 2, 3},{0, 0, 0},{-1, 1, 0},{5, -3, 2},}for _, c := range cases {c := c // 必须创建一个新的副本t.Run(fmt.Sprintf("%d+%d", c.a, c.b), func(t *testing.T) {t.Parallel()got := Add(c.a, c.b)if got != c.want {t.Errorf("Add(%d, %d) = %d; want %d", c.a, c.b, got, c.want)}})}
}
在这个例子中,我们使用t.Parallel()
来标记每个子测试可以并行运行。需要注意的是,每个子测试必须是线程安全的,因此我们在循环中创建了新的副本c
。
测试覆盖率 (Test Coverage)
测试覆盖率是一种度量测试代码质量的方法,它显示了哪些代码路径已经被测试覆盖。Go语言提供了内置的工具来生成测试覆盖率报告。
生成测试覆盖率报告:
go test -coverprofile=coverage.out
go tool cover -html=coverage.out -o coverage.html
第一行命令会生成一个覆盖率文件coverage.out
,第二行命令会将这个文件转换为HTML格式,以便于查看。
组织测试代码 (Organizing Test Code)
良好的测试代码组织结构可以使项目更加清晰和易于维护。通常,测试文件应与被测试的源文件放在同一个目录下,并以_test.go
结尾。
目录结构示例:
myproject/
├── add.go
├── add_test.go
└── main.go
每个源文件都应该有一个对应的测试文件,这样可以确保每个功能模块都有相应的测试。
总结
通过上述内容,我们可以看到Go语言的测试工具非常强大和灵活,能够满足各种测试需求。无论是简单的单元测试、复杂的并发测试还是详细的基准测试,Go语言都能提供有效的支持。希望这些内容对您有所帮助,如果您有任何问题或需要进一步的指导,请随时告诉我!
接下来我们将进一步探讨一些更高级的测试技巧和最佳实践,包括模拟(Mocking)、依赖注入、测试辅助函数、以及如何处理外部依赖。
模拟 (Mocking)
在测试中,模拟(Mocking)是一种常用的技术,用于隔离被测试代码与其他组件的依赖关系。通过模拟,我们可以控制外部依赖的行为,从而更好地测试我们的代码。
使用 gomock
进行模拟
gomock
是一个流行的 Go 语言模拟库,可以帮助我们生成和管理模拟对象。
安装 gomock
和 mockgen
:
go get github.com/golang/mock/mockgen
定义接口:
假设我们有一个 Calculator
接口,我们需要测试一个使用该接口的函数。
calculator.go:
package maintype Calculator interface {Add(a, b int) intSubtract(a, b int) int
}
生成模拟对象:
使用 mockgen
生成模拟对象。
mockgen -source=calculator.go -package=main > calculator_mock.go
生成的模拟对象:
calculator_mock.go:
package mainimport ("reflect""testing"
)// MockCalculator is a mock of Calculator interface
type MockCalculator struct {mock.Mock
}// Add provides a mock function with given fields: a, b
func (_m *MockCalculator) Add(a int, b int) int {ret := _m.Called(a, b)var r0 intif rf, ok := ret.Get(0).(func(int, int) int); ok {r0 = rf(a, b)} else {r0 = ret.Get(0).(int)}return r0
}// Subtract provides a mock function with given fields: a, b
func (_m *MockCalculator) Subtract(a int, b int) int {ret := _m.Called(a, b)var r0 intif rf, ok := ret.Get(0).(func(int, int) int); ok {r0 = rf(a, b)} else {r0 = ret.Get(0).(int)}return r0
}
测试代码:
calculator_test.go:
package mainimport ("testing""github.com/golang/mock/gomock"
)func TestUseCalculator(t *testing.T) {ctrl := gomock.NewController(t)defer ctrl.Finish()mockCalc := NewMockCalculator(ctrl)mockCalc.EXPECT().Add(1, 2).Return(3)mockCalc.EXPECT().Subtract(5, 3).Return(2)result := UseCalculator(mockCalc)if result != 5 {t.Errorf("UseCalculator() = %d; want 5", result)}
}func UseCalculator(calc Calculator) int {sum := calc.Add(1, 2)diff := calc.Subtract(5, 3)return sum + diff
}
在这个例子中,我们使用 gomock
生成了 Calculator
接口的模拟对象,并在测试中设置了期望的行为。然后,我们调用 UseCalculator
函数并验证其返回值。
依赖注入 (Dependency Injection)
依赖注入是一种设计模式,通过它可以在运行时将依赖项传递给对象,而不是在对象内部硬编码这些依赖项。这使得代码更灵活、更易于测试。
示例:
假设我们有一个 Service
类,它依赖于 Calculator
接口。
service.go:
package maintype Service struct {calc Calculator
}func NewService(calc Calculator) *Service {return &Service{calc: calc}
}func (s *Service) Compute(a, b int) int {sum := s.calc.Add(a, b)diff := s.calc.Subtract(a, b)return sum + diff
}
测试代码:
service_test.go:
package mainimport ("testing""github.com/golang/mock/gomock"
)func TestServiceCompute(t *testing.T) {ctrl := gomock.NewController(t)defer ctrl.Finish()mockCalc := NewMockCalculator(ctrl)mockCalc.EXPECT().Add(1, 2).Return(3)mockCalc.EXPECT().Subtract(1, 2).Return(-1)service := NewService(mockCalc)result := service.Compute(1, 2)if result != 2 {t.Errorf("Service.Compute() = %d; want 2", result)}
}
在这个例子中,我们通过构造函数将 Calculator
接口的实现传递给 Service
对象,从而实现了依赖注入。
测试辅助函数 (Helper Functions)
测试辅助函数可以帮助减少重复代码,使测试代码更加简洁和可维护。
示例:
假设我们有一个通用的辅助函数来验证两个整数是否相等。
test_helpers.go:
package mainimport ("testing"
)func AssertEqual(t *testing.T, got, want int, msg string) {if got != want {t.Errorf("%s: got %d, want %d", msg, got, want)}
}
使用辅助函数的测试代码:
add_test.go:
package mainimport ("testing"
)func TestAddWithHelper(t *testing.T) {cases := []struct {a, b, want int}{{1, 2, 3},{0, 0, 0},{-1, 1, 0},{5, -3, 2},}for _, c := range cases {got := Add(c.a, c.b)AssertEqual(t, got, c.want, fmt.Sprintf("Add(%d, %d)", c.a, c.b))}
}
在这个例子中,我们定义了一个 AssertEqual
辅助函数,并在测试代码中使用它来简化断言逻辑。
处理外部依赖 (Handling External Dependencies)
在测试中处理外部依赖(如数据库、网络服务等)是一个常见的挑战。通常的做法是使用内存数据库、模拟服务器或配置文件来模拟外部依赖。
示例:
假设我们有一个函数需要访问数据库。
database.go:
package mainimport ("database/sql""fmt"
)type DB interface {QueryRow(query string, args ...interface{}) *sql.Row
}func FetchUser(db DB, id int) (string, error) {var name stringerr := db.QueryRow("SELECT name FROM users WHERE id = ?", id).Scan(&name)if err != nil {return "", err}return name, nil
}
测试代码:
database_test.go:
package mainimport ("testing""github.com/DATA-DOG/go-sqlmock"
)func TestFetchUser(t *testing.T) {db, mock, err := sqlmock.New()if err != nil {t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)}defer db.Close()rows := sqlmock.NewRows([]string{"name"}).AddRow("Alice")mock.ExpectQuery("SELECT name FROM users WHERE id = ?").WithArgs(1).WillReturnRows(rows)name, err := FetchUser(db, 1)if err != nil {t.Errorf("unexpected error: %v", err)}if name != "Alice" {t.Errorf("expected Alice, got %s", name)}if err := mock.ExpectationsWereMet(); err != nil {t.Errorf("there were unfulfilled expectations: %s", err)}
}
在这个例子中,我们使用 go-sqlmock
库来模拟数据库连接和查询,从而避免了对实际数据库的依赖。
总结
通过上述内容,我们可以看到 Go 语言提供了丰富的工具和库来支持各种测试需求。从简单的单元测试到复杂的并发测试和外部依赖处理,Go 语言都提供了强大的支持。希望这些内容对您有所帮助,如果您有任何问题或需要进一步的指导,请随时告诉我!
相关文章:
第 22 章 - Go语言 测试与基准测试
在Go语言中,测试是一个非常重要的部分,它帮助开发者确保代码的正确性、性能以及可维护性。Go语言提供了一套标准的测试工具,这些工具可以帮助开发者编写单元测试、表达式测试(通常也是指单元测试中的断言)、基准测试等…...

VB.Net笔记-更新ing
目录 1.1 设置默认VS的开发环境为VB.NET(2024/11/18) 1.2 新建一个“Hello,world”的窗体(2024/11/18) 1.3 计算圆面积的小程序(2024/11/18) 显示/隐式 声明 (2024/11/18&…...

centos 服务器 docker 使用代理
宿主机使用代理 在宿主机的全局配置文件中添加代理信息 vim /etc/profile export http_proxyhttp://127.0.0.1:7897 export https_proxyhttp://127.0.0.1:7897 export no_proxy"localhost,127.0.0.1,::1,172.171.0.0" docker 命令使用代理 例如我想在使用使用 do…...
python语言基础
1. 基础语法 Q: Python 中的变量与数据类型有哪些? A: Python 支持多种数据类型,包括数字(整数 int、浮点数 float、复数 complex)、字符串 str、列表 list、元组 tuple、字典 dict 和集合 set。每种数据类型都有其特定的用途和…...
Python中的Apriori库详解
文章目录 Python中的Apriori库详解一、引言二、Apriori算法原理与Python实现1、Apriori算法原理2、Python实现1.1、数据准备1.2、转换数据1.3、计算频繁项集1.4、提取关联规则 三、案例分析1、导入必要的库2、准备数据集3、数据预处理4、应用Apriori算法5、生成关联规则6、打印…...
MongoDB比较查询操作符中英对照表及实例详解
mongodb比较查询操作符中英表格一览表 NameDescription功能$eqMatches values that are equal to a specified value.匹配值等于指定值。$gtMatches values that are greater than a specified value.匹配值大于指定值。$gteMatches values that are greater than or equal to…...

掌上单片机实验室 – RT-Thread + ROS2 初探(25)
在初步尝试RT-Thread之后,一直在琢磨如何进一步感受它的优点,因为前面只是用了它的内核,感觉和FreeRTOS、uCOS等RTOS差别不大,至于它们性能、可靠性上的差异,在这种学习性的程序中,很难有所察觉。 RT-Threa…...
Kotlin中的?.和!!主要区别
目录 1、?.和!!介绍 2、使用场景和最佳实践 3、代码示例和解释 1、?.和!!介绍 Kotlin中的?.和!!主要区别在于它们对空指针的处理方式。 ?.(安全调用操作符):当变量可能为null时,使用?.可以安全地调用其方法或属性…...

iframe嵌入踩坑记录
iframe嵌入父子页面token问题 背景介绍 最近在做在平台A中嵌入平台B某个页面的需求,我负责的是平台B这边,使这个页面被嵌入后能正常使用。两个平台都实现了单点登录。 其实这是第二次做这个功能了,原本以为会很顺利,但没想到折腾…...
面试小札:Java的类加载过程和类加载机制。
Java类加载过程 加载(Loading) 这是类加载过程的第一个阶段。在这个阶段,Java虚拟机(JVM)主要完成三件事: 通过类的全限定名来获取定义此类的二进制字节流。这可以从多种来源获取,如本地文件系…...
Spring 上下文对象
1. Spring 上下文对象概述 Spring 上下文对象(ApplicationContext)是 Spring 框架的核心接口之一,它扩展了 BeanFactory 接口,提供了更多企业级应用所需的功能。ApplicationContext 不仅可以管理 Bean 的生命周期和配置࿰…...

Wireshark抓取HTTPS流量技巧
一、工具准备 首先安装wireshark工具,官方链接:Wireshark Go Deep 二、环境变量配置 TLS 加密的核心是会话密钥。这些密钥由客户端和服务器协商生成,用于对通信流量进行对称加密。如果能通过 SSL/TLS 日志文件(例如包含密钥的…...

测试人员--如何区分前端BUG和后端BUG
在软件测试中,发现一个BUG并不算难,但准确定位它的来源却常常让测试人员头疼。是前端页面的问题?还是后台服务的异常?如果搞错了方向,开发人员之间的沟通效率会大大降低,甚至导致问题久拖不决。 那么&#…...

【Vue】指令扩充(指令修饰符、样式绑定)
目录 指令修饰符 按键修饰符 事件修饰符 双向绑定指令修饰符 输入框 表单域 下拉框 单选按钮 复选框 样式绑定 分类 绑定class 绑定style tab页切换示例 指令修饰符 作用 借助指令修饰符,可以让指令的功能更强大 分类 按键修饰符:用来…...
Ubuntu20.04 Rk3588 交叉编译ffmpeg7.0
firefly 公司出的rk3588的设备,其中已经安装了gcc 交叉编译工具,系统版本是Ubuntu20.04。 使用Ubuntu20.04 交叉编译ffmpeg_ubuntu下配置ffmpeg交叉编译器为arm-linux-gnueabihf-gcc-CSDN博客文章浏览阅读541次。ubuntu20.04 交叉编译ffmpeg_ubuntu下配…...

HTML常用表格与标签
一、table表格标签: <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Title</title> </head> <body> <!--有大小为1的边框--> <table border"1">…...

网络安全与加密
1.Base64简单说明描述:Base64可以成为密码学的基石,非常重要。特点:可以将任意的二进制数据进行Base64编码结果:所有的数据都能被编码为并只用65个字符就能表示的文本文件。65字符:A~Z a~z 0~9 / 对文件进行base64编码…...
MySQL数据库-索引的介绍和使用
目录 MySQL数据库-索引1.索引介绍2.索引分类3.创建索引3.1 唯一索引3.2 普通索引3.3 组合索引3.4 全文索引 4.索引使用5.查看索引6.删除索引7.索引总结7.1 优点7.2 缺点7.3 索引使用注意事项 MySQL数据库-索引 数据库是用来存储数据,在互联网应用中,数据…...
【图像去噪】论文精读:Pre-Trained Image Processing Transformer(IPT)
请先看【专栏介绍文章】:【图像去噪(Image Denoising)】关于【图像去噪】专栏的相关说明,包含适配人群、专栏简介、专栏亮点、阅读方法、定价理由、品质承诺、关于更新、去噪概述、文章目录、资料汇总、问题汇总(更新中) 文章目录 前言Abstract1. Introduction2. Related…...
Java SE 与 Java EE:基础与进阶的探索之旅
在编程世界中,Java语言以其跨平台、面向对象、丰富的类库等特点,成为了众多开发者和企业的首选编程语言。而Java SE与Java EE,作为Java平台的两个重要组成部分,各自承载着不同的使命,同时又紧密相连,共同构…...

AI-调查研究-01-正念冥想有用吗?对健康的影响及科学指南
点一下关注吧!!!非常感谢!!持续更新!!! 🚀 AI篇持续更新中!(长期更新) 目前2025年06月05日更新到: AI炼丹日志-28 - Aud…...

深入浅出Asp.Net Core MVC应用开发系列-AspNetCore中的日志记录
ASP.NET Core 是一个跨平台的开源框架,用于在 Windows、macOS 或 Linux 上生成基于云的新式 Web 应用。 ASP.NET Core 中的日志记录 .NET 通过 ILogger API 支持高性能结构化日志记录,以帮助监视应用程序行为和诊断问题。 可以通过配置不同的记录提供程…...

VB.net复制Ntag213卡写入UID
本示例使用的发卡器:https://item.taobao.com/item.htm?ftt&id615391857885 一、读取旧Ntag卡的UID和数据 Private Sub Button15_Click(sender As Object, e As EventArgs) Handles Button15.Click轻松读卡技术支持:网站:Dim i, j As IntegerDim cardidhex, …...

循环冗余码校验CRC码 算法步骤+详细实例计算
通信过程:(白话解释) 我们将原始待发送的消息称为 M M M,依据发送接收消息双方约定的生成多项式 G ( x ) G(x) G(x)(意思就是 G ( x ) G(x) G(x) 是已知的)࿰…...

【JavaWeb】Docker项目部署
引言 之前学习了Linux操作系统的常见命令,在Linux上安装软件,以及如何在Linux上部署一个单体项目,大多数同学都会有相同的感受,那就是麻烦。 核心体现在三点: 命令太多了,记不住 软件安装包名字复杂&…...

网络编程(UDP编程)
思维导图 UDP基础编程(单播) 1.流程图 服务器:短信的接收方 创建套接字 (socket)-----------------------------------------》有手机指定网络信息-----------------------------------------------》有号码绑定套接字 (bind)--------------…...
Element Plus 表单(el-form)中关于正整数输入的校验规则
目录 1 单个正整数输入1.1 模板1.2 校验规则 2 两个正整数输入(联动)2.1 模板2.2 校验规则2.3 CSS 1 单个正整数输入 1.1 模板 <el-formref"formRef":model"formData":rules"formRules"label-width"150px"…...
Java多线程实现之Thread类深度解析
Java多线程实现之Thread类深度解析 一、多线程基础概念1.1 什么是线程1.2 多线程的优势1.3 Java多线程模型 二、Thread类的基本结构与构造函数2.1 Thread类的继承关系2.2 构造函数 三、创建和启动线程3.1 继承Thread类创建线程3.2 实现Runnable接口创建线程 四、Thread类的核心…...

Maven 概述、安装、配置、仓库、私服详解
目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...
力扣-35.搜索插入位置
题目描述 给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。 请必须使用时间复杂度为 O(log n) 的算法。 class Solution {public int searchInsert(int[] nums, …...