golang13 单元测试
摘要
该视频主要讲述了Go语言的测试和日志设计。首先强调了单元测试在项目中的重要性,介绍了如何编写可运行的测试代码,包括测试结果的报告方法。其次提到了日志在开发中的重要性,并表示将在后续章节中详细讲解如何设计一个日志包。此外,还通过实例演示了如何在IDE中运行测试,展示了如何查看测试覆盖率和运行多种测试方案。最后强调了写测试用例的简单性和快速性。
分段总结
折叠
00:01单元测试的重要性
1.单元测试在生产级项目开发中非常重要。 2.即使初创项目可能没有写单元测试,但核心项目都会编写单元测试。
01:07Go语言中的单元测试
1.Go语言中运行单元测试的命令是go test。 2.go test命令会运行包目录中所有以test.go结尾的源码文件。 3.这些测试文件不会被go build命令打包到最终的可执行文件中。
02:49测试文件的分类
1.测试文件分为功能测试(以test开头)和性能测试(以benchmark开头)。 2.还有示例测试和模糊测试,这些在后续阶段会讲解。
03:38编写单元测试用例
1.定义要测试的函数,如add函数,接受两个int类型的参数并返回它们的和。 2.推荐使用test测试用例,并与包放在同一目录下。 3.测试函数以test开头,接受testing.T类型的参数。
06:08报告测试结果
1.使用testing.T类型的t的Error和Fatal方法报告测试结果。 2.Error方法用于报告测试错误,Fatal方法用于报告致命错误并终止测试。
08:25运行单元测试
1.在IDE或命令行中运行go test命令来执行单元测试。 2.可以使用不同的运行选项,如普通运行、调试运行和覆盖率检查。
重点
本视频暂不支持提取重点
一、单元测试 00:02
1. 单元测试概述 00:53
重要性: 在实际生产级项目开发中非常重要,虽然初创项目可能不写,但当项目成为核心项目后都需要编写核心单元测试
测试命令: 使用go test命令运行测试用例,该命令是按照约定组织的测试代码驱动程序
2. 写单元测试用例 03:35
1)写单元测试用例的定义及流程
定义函数并编写测试用例
03:42
文件命名: 测试文件必须以_test.go结尾,如add_test.go
文件位置: 测试文件需要与被测试代码放在同一个包中,以便测试内部函数
测试分类
:
Test开头:功能测试
Benchmark开头:性能测试
Example开头:样本测试
模糊测试:在高级阶段讲解
函数定义: 测试函数必须以Test开头,后接函数名,参数为t *testing.T
错误报告
:
t.Errorf(): 报告格式化错误
t.Error(): 报告简单错误
t.Fail(): 标记测试失败但不终止
t.FailNow(): 立即终止测试
运行测试用例
08:17
命令行运行: 在包目录下执行go test或go test .
IDE运行: 可以直接点击测试函数旁边的运行按钮
测试结果
:
通过:显示PASS
失败:显示FAIL和错误信息
点击run
08:50
运行选项
:
直接运行(Run)
调试运行(Debug)
带覆盖率运行(Run with Coverage)
覆盖率检查: 可以显示代码被测试覆盖的百分比,100%表示所有代码都被测试到
二、知识小结
知识点 | 核心内容 | 考试重点/易混淆点 | 难度系数 |
---|---|---|---|
Go语言单元测试重要性 | 生产级项目逐步需要补充单元测试,初创项目常忽略 | 核心项目必须写单元测试 vs 初创项目可暂缓 | ⭐⭐ |
测试文件命名规范 | 以_test.go结尾,go build不会打包进可执行文件 | 测试文件与生产代码同包 vs Java的独立test包 | ⭐⭐ |
测试类型分类 | 功能测试(Test开头)、性能测试(Benchmark开头)、示例测试(Example)、模糊测试 | 功能测试与性能测试的语法差异 | ⭐⭐⭐ |
测试函数结构 | func TestXxx(t *testing.T)固定格式,IDE自动识别可运行标记 | 参数必须为*testing.T,否则无法触发测试 | ⭐⭐ |
断言与错误报告 | 使用t.Error/f系列方法,支持格式化输出预期值与实际值 | Errorf与Fatal的中断测试行为差异 | ⭐⭐⭐ |
测试覆盖率检查 | 通过go test -cover或IDE的Run with Coverage查看 | 100%覆盖率不等于100%正确性 | ⭐⭐⭐⭐ |
命令行测试执行 | go test自动运行匹配文件,支持子目录级测试 | 包内测试 vs 跨包测试的可见性问题 | ⭐⭐ |
一、单元测试 00:02
基本结构:Go语言单元测试文件以_test.go结尾,包含TestXxx函数,参数为*testing.T
断言方法:使用t.Errorf()输出错误信息,格式为"expect %d, actual %d",其中%d为占位符
二、跳过耗时的单元测试用例 00:26
1. 单元测试用例的耗时测试 00:39
问题场景:当测试文件包含10-20个测试用例时,某些用例可能非常耗时
需求分析:需要选择性跳过耗时测试,只运行关键测试用例
2. 单元测试用例的跳过方法 00:49
核心方法:使用testing.Short()判断是否处于短测试模式
跳过机制:通过t.Skip("跳过原因")主动跳过当前测试用例
执行控制:运行时添加-short参数可触发跳过逻辑
3. 例题1:测试add函数并跳过耗时测试 01:19
实现步骤
:
在耗时测试函数开始处添加if testing.Short() { t.Skip() }
正常编写测试逻辑(如add(1,5)应返回6)
验证方法:通过fmt.Println输出验证是否执行跳过
执行命令
:
普通模式:go test
短测试模式:go test -short
4. 单元测试用例的short模式 02:01
模式特点
:
短测试模式下会跳过标记的测试用例
非短测试模式下执行全部测试
实际应用:适合耗时长的初始化测试、性能测试等场景
注意事项:短测试的判断标准由开发者自行定义,没有固定时间阈值
5. 单元测试用例的总结与后续 02:57
当前范围:本章节仅介绍基础单元测试用法
进阶内容:后续会有专门章节讲解生产环境中的测试细节
开发建议:测试代码应与业务代码同步维护,保证测试覆盖率
三、知识小结
知识点 | 核心内容 | 考试重点/易混淆点 | 难度系数 |
---|---|---|---|
单元测试批量运行 | 通过go test命令运行多个测试用例 | 默认执行全部测试,耗时用例影响效率 | ⭐⭐ |
跳过耗时测试方法 | 使用testing.Short()+t.Skip()控制跳过逻辑 | -short参数触发条件判断 | ⭐⭐⭐ |
条件测试代码示例 | if testing.Short() { t.Skip() }结构体 | 需明确short模式下的替代逻辑 | ⭐⭐ |
表格驱动测试预告 | 下节课讲解基于表格的测试用例设计 | 参数化测试与批量断言对比 | ⭐⭐ |
一、基于表格的测试数据管理 00:00
1. 问题导入 00:08
维护性问题:当测试数据有多组时,为每组数据单独编写测试用例会导致代码冗余且难以维护
边界测试困难:传统方法难以系统性地覆盖边界条件测试,如负数相加、零值相加等情况
2. 表格驱动的测试编码 00:24
数据结构设计
:
使用匿名结构体定义测试数据集,包含输入参数a(int)、b(int)和预期输出out(int)
结构体实例化时可添加多组测试数据,如:{a:1,b:1,out:2}、{a:-9,b:8,out:-1}等
测试循环结构
:
通过for _, value := range dataset遍历所有测试用例
每组数据调用被测函数后,用if re != value.out判断实际结果是否符合预期
错误报告
:
使用t.Errorf("expect: %d, actual:%d", value.out,re)格式输出详细错误信息
错误信息会明确显示预期值和实际值的差异
3. 测试示例 03:25
正确案例:当所有测试数据正确时,显示"PASS: TestAdd (0.00s)"
错误案例
:
修改预期值(如将0+0=0改为0+0=1)会触发测试失败
错误信息会精确定位到出错的数据组和具体值差异
边界测试
:
示例包含正数相加(12+12=24)
负数相加(-9+8=-1)
零值相加(0+0=0)等多种边界情况
二、知识小结
知识点 | 核心内容 | 考试重点/易混淆点 | 难度系数 |
---|---|---|---|
表格驱动测试 | 通过匿名结构体定义多组测试数据(输入a/b,预期out),用循环遍历数据执行断言 | 结构体字段与测试逻辑的映射关系 | ⭐⭐ |
测试数据管理 | 集中维护测试数据集(如常规值、边界值),避免重复编写用例 | 边界条件覆盖(如负数、零值) | ⭐⭐ |
错误检测机制 | 通过t.Error输出预期值与实际值差异(格式化字符串比对) | 错误信息清晰度优化 | ⭐⭐ |
性能测试预告 | 下节课将讲解性能测试方法论 | 与单元测试的差异点 | ⭐⭐⭐ |
摘要
该视频主要讲述了性能测试的概念和如何进行性能测试。性能测试是针对核心函数的测试,旨在确保这些函数的性能达到预期。性能测试可以通过编写bench mark函数来实现,该函数使用特定的测试数据对函数进行多轮测试,并记录每轮测试的执行时间和性能指标。视频还介绍了如何通过编写不同的bench mark函数来比较不同方法的性能差异,例如字符串拼接的方式。通过这种方式,可以找出最优的算法或方法,提高程序的执行效率。
分段总结
折叠
00:01性能测试概述
1.性能测试的核心目标是评估核心函数的性能。 2.性能测试的关键词是bench,测试框架为benchmark。
01:09性能测试的基本语法
1.性能测试的基本语法包括函数定义、测试数据和测试轮次。 2.函数定义包括函数名、参数类型和返回值类型。 3.测试数据通过循环进行多轮测试,以确保结果的可靠性。 4.测试轮次可以通过命令行参数传递给测试程序。
03:46字符串拼接的性能测试
1.字符串拼接有三种方式:sprintf、加号和string builder。 2.通过benchmark测试这三种方式的性能差异。 3.测试结果显示,string builder的性能远高于sprintf和加号。 4.string builder的使用方式简单,且性能优异,推荐使用。
重点
本视频暂不支持提取重点
一、性能测试 00:00
1. 性能测试的写法 00:33
1)示例 00:35
核心函数测试:性能测试主要针对核心函数而非所有函数,核心函数的性能对系统至关重要
测试结构
:
使用func BenchmarkXxx(b *testing.B)作为性能测试函数命名规范
参数为testing.B类型而非普通测试的testing.T
必须调用b.ResetTimer()重置计时器
循环机制
:
通过for i := 0; i < b.N; i++进行多轮测试
b.N由go test自动确定测试次数,确保结果可靠性
结果分析
:
输出包含执行次数和每次操作耗时(如0.2599纳秒)
示例中add函数执行时间为0.2599纳秒
错误处理
:
可使用fmt.Printf打印中间结果但不影响性能统计
主要关注执行时间而非正确性验证
计时控制
:
测试结束需调用b.StopTimer()
避免准备数据的耗时被计入测试结果
2. 字符串拼接的方法 03:58
1)字符串拼接的方法介绍
三种方法
:
fmt.Sprintf格式化拼接
直接使用+运算符相加
strings.Builder高效构建
测试设置
:
定义常量const numbers = 10000控制循环次数
每种方法独立编写Benchmark函数
例题:字符串拼接性能测试
04:10
Sprintf实现
:
使用str = fmt.Sprintf("%s%d", str, j)方式拼接
需要处理格式化字符串开销
直接相加
:
使用str += strconv.Itoa(j)方式
每次操作产生新字符串对象
Builder实现
:
使用builder.WriteString(strconv.Itoa(j))
最后调用builder.String()获取结果
内存预分配减少拷贝
各方法性能分析
09:44
性能对比
:
Sprintf: 25470681 ns/op
直接相加: 23501926 ns/op
Builder: 286006 ns/op
性能差异
:
Builder比前两种快约90倍
Sprintf和直接相加性能接近
使用建议
:
追求性能时优先使用Builder
简单场景可使用直接相加(代码更简洁)
需要格式化时使用Sprintf
二、知识小结
知识点 | 核心内容 | 考试重点/易混淆点 | 难度系数 |
---|---|---|---|
性能测试基础概念 | 核心函数性能测试的重要性与实施场景区分 | 核心函数与普通函数的测试差异 | ⭐⭐ |
Benchmark编写规范 | benchmark关键字使用与testing.B参数规范 | b.N循环控制与ResetTimer()调用时机 | ⭐⭐⭐ |
数值计算性能测试 | Add函数基准测试实现与纳秒级耗时分析 | 多轮测试必要性(避免单次测试偏差) | ⭐⭐ |
字符串拼接性能对比 | Sprintf/+操作符/strings.Builder三种实现方式 | 90倍性能差距(Builder最优) | ⭐⭐⭐⭐ |
测试结果解读 | 运行次数统计(亿级)与纳秒级耗时分析 | 性能对比计算公式与统计显著性 | ⭐⭐⭐ |
最佳实践总结 | 高频操作场景推荐使用strings.Builder | 性能与代码可读性的平衡点 | ⭐⭐ |
以下是关于 Go 语言单元测试的 20 道八股文题(难度递增)和 15 道场景题(涵盖基础到高级应用),系统覆盖单元测试的概念、工具及实践技巧。
一、八股文题(20 题)
基础篇(1-8 题)
问题:Go 语言的单元测试是什么?其主要目的是什么? 答案:
定义:单元测试是针对程序中最小可测试单元(如函数、方法)的测试,验证其在各种输入下的行为是否符合预期。
主要目的:
确保单个组件功能正确性。
及早发现代码缺陷,降低修复成本。
支持重构,保证修改后功能不受影响。
作为代码文档,展示如何使用组件。
问题:Go 单元测试的文件命名规则和函数命名规则是什么? 答案:
测试文件命名:必须以
_test.go
结尾(如math_test.go
对应math.go
)。测试函数命名:必须以
Test
开头,参数为*testing.T
,格式为func TestXxx(t *testing.T)
。示例:
func TestAdd(t *testing.T) { ... }
问题:
testing
包的核心类型有哪些?*testing.T
的常用方法有哪些? 答案:核心类型:
*testing.T
(测试控制)、*testing.B
(基准测试)、testing.TB
(T 和 B 的接口)。*testing.T
常用方法:
t.Error(args...)
:记录错误但继续执行。t.Fatal(args...)
:记录错误并终止当前测试。t.Log(args...)
:打印日志信息。t.Run(name, func)
:运行子测试。t.Skip(args...)
:跳过当前测试。
问题:如何运行 Go 单元测试?
go test
命令的常用参数有哪些? 答案:运行测试:在包目录执行
go test
,或指定包路径go test ./mypkg
。常用参数:
-v
:显示详细测试输出。-run 模式
:只运行名称匹配模式的测试(如-run TestAdd
)。-cover
:显示代码覆盖率。-count n
:指定测试运行次数(默认 1)。-short
:运行短测试(跳过耗时测试)。
问题:什么是测试覆盖率?如何查看和分析 Go 代码的测试覆盖率? 答案:
定义:被测试覆盖的代码占总代码的比例,衡量测试的全面性。
查看方法:
基本覆盖率:
go test -cover
(输出百分比)。生成覆盖率报告:
go test -coverprofile=cover.out
。可视化分析:
go tool cover -html=cover.out
(生成 HTML 报告)。
问题:如何编写一个简单的单元测试?请举例说明测试函数的基本结构。 答案: 示例:测试
Add
函数go
// math.go package mathutilfunc Add(a, b int) int {return a + b }// math_test.go package mathutilimport "testing"func TestAdd(t *testing.T) {// 测试用例cases := []struct {name stringa, b intwant int}{{"positive numbers", 2, 3, 5},{"negative numbers", -1, -2, -3},{"mixed signs", -1, 1, 0},}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)}})} }
问题:子测试(subtest)的作用是什么?如何使用
t.Run
创建子测试? 答案:作用:将一个测试函数拆分为多个相关子测试,便于单独运行和组织测试用例,失败时能精确定位。
使用方法:通过
t.Run(name, func(t *testing.T) { ... })
创建,第一个参数为子测试名称,第二个为测试函数。示例:见上题中的
t.Run(c.name, ...)
,可单独运行go test -run TestAdd/positive
。
问题:
t.Error
和t.Fatal
的区别是什么?分别在什么场景下使用? 答案:区别:
t.Error
:记录错误信息,继续执行当前测试函数的后续代码。t.Fatal
:记录错误信息并调用t.FailNow()
,立即终止当前测试函数(后续代码不执行)。
场景:
t.Error
:适合非致命错误,希望继续执行其他测试用例。t.Fatal
:适合致命错误(如初始化失败),继续执行无意义的场景。
中级篇(9-15 题)
问题:什么是基准测试(benchmark)?其函数命名和参数有什么要求? 答案:
定义:用于测量代码性能(执行时间)的测试,帮助分析和优化代码效率。
命名规则:函数名以
Benchmark
开头,参数为*testing.B
,格式为func BenchmarkXxx(b *testing.B) { ... }
。执行:
go test -bench=.
(运行所有基准测试),-benchmem
可显示内存分配。示例:
go
func BenchmarkAdd(b *testing.B) {for i := 0; i < b.N; i++ {Add(1, 2) // 被测试代码,b.N为动态调整的迭代次数} }
问题:如何测试带有外部依赖(如数据库、网络)的函数?什么是测试替身? 答案:
测试方法:使用测试替身(Test Double)替代真实外部依赖,隔离被测试代码。
测试替身类型:
模拟(Mock):验证交互行为(如是否调用特定方法)。
存根(Stub):返回预设值,不验证交互。
假实现(Fake):简化的真实实现(如内存数据库)。
示例:用接口定义依赖,测试时传入模拟实现。
问题:
table-driven test
(表格驱动测试)的特点是什么?如何实现? 答案:特点:将多个测试用例组织在切片中,通过循环执行,代码简洁、易扩展,新增用例只需添加表格行。
实现步骤:
定义包含输入、预期输出和名称的结构体切片。
循环遍历切片,用子测试执行每个用例。
示例:见第 6 题中的
cases
切片实现。
问题:如何跳过某些测试?
t.Skip
和-short
flag 如何配合使用? 答案:跳过测试:在测试函数中调用
t.Skip("原因")
,该测试会被标记为跳过。与
-short
配合:通过
testing.Short()
判断是否启用短模式,跳过耗时测试:
go
运行
func TestLongRunning(t *testing.T) {if testing.Short() {t.Skip("短模式下跳过耗时测试")}// 执行耗时测试... }
运行:
go test -short
会跳过该测试。
问题:如何测试错误返回?如何验证错误信息的正确性? 答案:
测试方法:检查函数返回的错误是否为
nil
(预期成功时)或非nil
(预期失败时),并验证错误内容。验证错误信息:
使用
errors.Is
检查错误类型或链。使用
strings.Contains
检查错误消息内容。
示例:
go
运行
func TestDivide(t *testing.T) {_, err := Divide(5, 0)if err == nil {t.Fatal("预期错误,但未返回错误")}if !strings.Contains(err.Error(), "除数不能为零") {t.Errorf("错误信息不正确: %v", err)} }
问题:什么是模糊测试(fuzzing)?Go 如何支持模糊测试? 答案:
定义:自动生成大量随机输入测试函数,发现边界情况和潜在漏洞的测试方法。
Go 支持(1.18+):
函数命名:
func FuzzXxx(f *testing.F) { ... }
。步骤:添加种子输入(
f.Add(...)
),定义测试逻辑(f.Fuzz(func(t *testing.T, input 类型) { ... })
)。运行:
go test -fuzz=.
。
问题:如何测试私有函数(未导出函数)?有哪些最佳实践? 答案:
测试方法:
在同一包内的测试文件中直接调用(推荐,因测试文件属于同一包)。
重构:将私有函数逻辑提取为导出函数(不推荐,破坏封装)。
最佳实践:优先通过测试公共函数间接测试私有函数,必要时在同包测试文件中直接测试。
高级篇(16-20 题)
问题:如何为 HTTP handler 编写单元测试?如何模拟请求和响应? 答案:
方法:使用
net/http/httptest
包创建模拟请求(NewRequest
)和记录响应(NewRecorder
)。示例:
go
运行
func TestHelloHandler(t *testing.T) {// 创建测试请求req := httptest.NewRequest("GET", "/hello?name=Alice", nil)// 创建响应记录器w := httptest.NewRecorder()// 调用handlerHelloHandler(w, req)// 获取响应resp := w.Result()if resp.StatusCode != http.StatusOK {t.Errorf("状态码错误: got %d, want %d", resp.StatusCode, http.StatusOK)}// 验证响应体body, _ := io.ReadAll(resp.Body)if string(body) != "Hello, Alice!" {t.Errorf("响应内容错误: %s", body)} }
问题:测试中的 Setup 和 Teardown 如何实现?如何在多个测试间共享资源? 答案:
实现方式:
包级 Setup:在
TestMain
中执行,所有测试前运行 Setup,测试后运行 Teardown。测试内 Setup:在测试函数中调用通用初始化函数。
示例(
TestMain
):
go
运行
func TestMain(m *testing.M) {// Setup: 初始化资源(如数据库连接)setup()// 运行所有测试code := m.Run()// Teardown: 清理资源teardown()os.Exit(code) }
问题:如何使用第三方测试框架(如 Testify)简化测试代码?与标准库相比有何优势? 答案:
Testify 使用:提供
assert
和require
包简化断言,mock
包支持模拟对象。优势:
断言更简洁(
assert.Equal(t, want, got)
vs 手动判断)。内置常用测试模式(如错误断言、包含判断)。
简化 mock 对象创建。
示例:
go
运行
import "github.com/stretchr/testify/assert"func TestAdd(t *testing.T) {got := Add(2, 3)assert.Equal(t, 5, got, "Add(2,3) 应该返回5") }
问题:如何测试并发代码?有哪些工具和方法可以检测竞态条件? 答案:
测试方法:
使用
-race
flag 检测竞态条件:go test -race
。编写并发测试用例,启动多个 goroutine 操作共享资源。
使用
sync.WaitGroup
等待所有 goroutine 完成。
示例:
go
运行
func TestCounterConcurrent(t *testing.T) {var c Countervar wg sync.WaitGroupfor i := 0; i < 1000; i++ {wg.Add(1)go func() {defer wg.Done()c.Increment()}()}wg.Wait()if c.Value() != 1000 {t.Errorf("并发计数错误: got %d, want 1000", c.Value())} }
问题:单元测试的最佳实践有哪些?如何编写高质量的测试代码? 答案:
最佳实践:
测试应独立、可重复(不依赖外部状态)。
测试失败时提供清晰的错误信息。
覆盖边界条件(空输入、极值、错误情况)。
测试代码与生产代码保持同等质量。
避免测试实现细节(测试行为而非实现)。
定期运行测试,结合 CI/CD 自动化测试。
二、场景题(15 题)
基础应用(1-5 题)
场景:为一个整数切片工具函数编写单元测试,包括求最大值、最小值和求和功能。 答案:
go
运行
// sliceutil/sliceutil.go package sliceutilimport "errors"// Max 返回切片最大值,切片为空返回错误 func Max(nums []int) (int, error) {if len(nums) == 0 {return 0, errors.New("空切片")}max := nums[0]for _, n := range nums[1:] {if n > max {max = n}}return max, nil }// Min 返回切片最小值,切片为空返回错误 func Min(nums []int) (int, error) {if len(nums) == 0 {return 0, errors.New("空切片")}min := nums[0]for _, n := range nums[1:] {if n < min {min = n}}return min, nil }// Sum 返回切片元素之和 func Sum(nums []int) int {sum := 0for _, n := range nums {sum += n}return sum }// sliceutil/sliceutil_test.go package sliceutilimport ("errors""testing" )func TestMax(t *testing.T) {tests := []struct {name stringinput []intwant intwantErr error}{{"单元素", []int{5}, 5, nil},{"多元素", []int{1, 3, 2}, 3, nil},{"含负数", []int{-1, -3, -2}, -1, nil},{"空切片", []int{}, 0, errors.New("空切片")},}for _, tt := range tests {t.Run(tt.name, func(t *testing.T) {got, err := Max(tt.input)if !errors.Is(err, tt.wantErr) {t.Errorf("Max() error = %v, wantErr %v", err, tt.wantErr)return}if got != tt.want {t.Errorf("Max() = %v, want %v", got, tt.want)}})} }// TestMin 和 TestSum 类似,省略实现
场景:为一个字符串反转函数编写单元测试,覆盖普通字符串、空字符串、包含中文字符的情况。 答案:
go
运行
// strutil/strutil.go package strutilfunc Reverse(s string) string {runes := []rune(s)for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {runes[i], runes[j] = runes[j], runes[i]}return string(runes) }// strutil/strutil_test.go package strutilimport "testing"func TestReverse(t *testing.T) {cases := []struct {name stringinput stringwant string}{{"空字符串", "", ""},{"单字符", "a", "a"},{"英文字符", "hello", "olleh"},{"中文字符", "你好世界", "界世好你"},{"混合字符", "ab你c", "c你ba"},{"包含空格", "hello world", "dlrow olleh"},}for _, c := range cases {t.Run(c.name, func(t *testing.T) {got := Reverse(c.input)if got != c.want {t.Errorf("Reverse(%q) = %q, want %q", c.input, got, c.want)}})} }
场景:编写基准测试,比较
strings.Builder
和+
运算符拼接字符串的性能差异。 答案:go
运行
// bench_test.go package mainimport ("strings""testing" )const str = "test" const iterations = 1000// 测试 + 运算符拼接 func BenchmarkStringConcatPlus(b *testing.B) {for i := 0; i < b.N; i++ {s := ""for j := 0; j < iterations; j++ {s += str}} }// 测试 strings.Builder 拼接 func BenchmarkStringConcatBuilder(b *testing.B) {for i := 0; i < b.N; i++ {var builder strings.Builderbuilder.Grow(iterations * len(str)) // 预分配内存for j := 0; j < iterations; j++ {builder.WriteString(str)}_ = builder.String()} }// 运行命令:go test -bench=. -benchmem // 预期结果:Builder性能远优于+运算符,内存分配更少
场景:使用子测试和表格驱动测试,测试一个简单的用户注册验证函数(检查用户名、密码合法性)。 答案:
go
运行
// user/validator.go package userimport "strings"// ValidateRegistration 验证用户注册信息 // 用户名:3-10个字符,密码:6-20个字符 func ValidateRegistration(username, password string) error {if len(username) < 3 || len(username) > 10 {return Errorf("用户名长度必须为3-10个字符")}if len(password) < 6 || len(password) > 20 {return Errorf("密码长度必须为6-20个字符")}return nil }// 自定义错误类型 type ValidationError stringfunc (e ValidationError) Error() string { return string(e) } func Errorf(format string, v ...interface{}) error {return ValidationError(fmt.Sprintf(format, v...)) }// user/validator_test.go package userimport ("testing" )func TestValidateRegistration(t *testing.T) {tests := []struct {name stringusername stringpassword stringwantErr boolerrMsg string}{{"合法信息", "alice", "password123", false, ""},{"用户名过短", "ab", "password123", true, "用户名长度必须为3-10个字符"},{"用户名过长", "alice123456", "password123", true, "用户名长度必须为3-10个字符"},{"密码过短", "alice", "123", true, "密码长度必须为6-20个字符"},{"密码过长", "alice", "123456789012345678901", true, "密码长度必须为6-20个字符"},}for _, tt := range tests {t.Run(tt.name, func(t *testing.T) {err := ValidateRegistration(tt.username, tt.password)if (err != nil) != tt.wantErr {t.Errorf("ValidateRegistration() error = %v, wantErr %v", err, tt.wantErr)return}if tt.wantErr && err.Error() != tt.errMsg {t.Errorf("错误信息不匹配: got %q, want %q", err.Error(), tt.errMsg)}})} }
场景:测试一个可能返回多种错误类型的函数,使用
errors.Is
和errors.As
验证错误链。 答案:go
运行
// store/store.go package storeimport "errors"var (ErrNotFound = errors.New("资源不存在")ErrInvalidID = errors.New("无效的ID") )// GetResource 根据ID获取资源 func GetResource(id string) (string, error) {if id == "" {return "", ErrInvalidID}if id != "valid123" {return "", ErrNotFound}return "resource data", nil }// store/store_test.go package storeimport ("errors""testing" )func TestGetResource(t *testing.T) {tests := []struct {name stringid stringwant stringwantErr error}{{"有效ID", "valid123", "resource data", nil},{"无效ID", "", "", ErrInvalidID},{"未找到", "invalid456", "", ErrNotFound},}for _, tt := range tests {t.Run(tt.name, func(t *testing.T) {got, err := GetResource(tt.id)if got != tt.want {t.Errorf("GetResource() = %v, want %v", got, tt.want)}// 验证错误类型if !errors.Is(err, tt.wantErr) {t.Errorf("GetResource() error = %v, wantErr %v", err, tt.wantErr)}})} }
中级应用(6-10 题)
场景:为一个依赖数据库的用户服务编写单元测试,使用接口和模拟对象隔离数据库依赖。 答案:
go
运行
// user/service.go package user// User 用户模型 type User struct {ID stringName string }// UserStore 数据库操作接口 type UserStore interface {GetByID(id string) (*User, error)Save(user *User) error }// Service 用户服务 type Service struct {store UserStore }func NewService(store UserStore) *Service {return &Service{store: store} }// GetUser 获取用户 func (s *Service) GetUser(id string) (*User, error) {return s.store.GetByID(id) }// user/service_test.go package userimport ("errors""testing" )// MockStore 模拟数据库实现 type MockStore struct {users map[string]*Usererr error }func NewMockStore(users map[string]*User, err error) *MockStore {return &MockStore{users: users, err: err} }func (m *MockStore) GetByID(id string) (*User, error) {if m.err != nil {return nil, m.err}user, ok := m.users[id]if !ok {return nil, errors.New("用户不存在")}return user, nil }func (m *MockStore) Save(user *User) error {return m.err }func TestService_GetUser(t *testing.T) {tests := []struct {name stringmockErr errorusers map[string]*Userid stringwant *UserwantErr bool}{{name: "用户存在",users: map[string]*User{"1": {ID: "1", Name: "Alice"}},id: "1",want: &User{ID: "1", Name: "Alice"},wantErr: false,},{name: "用户不存在",users: map[string]*User{},id: "99",want: nil,wantErr: true,},{name: "数据库错误",mockErr: errors.New("连接失败"),id: "1",want: nil,wantErr: true,},}for _, tt := range tests {t.Run(tt.name, func(t *testing.T) {store := NewMockStore(tt.users, tt.mockErr)service := NewService(store)got, err := service.GetUser(tt.id)if (err != nil) != tt.wantErr {t.Errorf("GetUser() error = %v, wantErr %v", err, tt.wantErr)return}if !equalUser(got, tt.want) {t.Errorf("GetUser() = %v, want %v", got, tt.want)}})} }func equalUser(a, b *User) bool {if a == nil && b == nil {return true}if a == nil || b == nil {return false}return a.ID == b.ID && a.Name == b.Name }
场景:编写测试覆盖一个带有默认参数的函数,测试不同参数组合的情况。 答案:
go
运行
// config/config.go package config// Option 配置选项 type Option func(*Config)// Config 配置结构体 type Config struct {Timeout intRetries intLogLevel string }// WithTimeout 设置超时时间 func WithTimeout(timeout int) Option {return func(c *Config) {c.Timeout = timeout} }// WithRetries 设置重试次数 func WithRetries(retries int) Option {return func(c *Config) {c.Retries = retries} }// NewConfig 创建配置,带默认值 func NewConfig(opts ...Option) *Config {// 默认值cfg := &Config{Timeout: 5, // 默认5秒Retries: 3, // 默认3次LogLevel: "info",}// 应用选项for _, opt := range opts {opt(cfg)}return cfg }// config/config_test.go package configimport "testing"func TestNewConfig(t *testing.T) {tests := []struct {name stringopts []Optionwant *Config}{{name: "默认配置",opts: []Option{},want: &Config{Timeout: 5, Retries: 3, LogLevel: "info"},},{name: "自定义超时",opts: []Option{WithTimeout(10)},want: &Config{Timeout: 10, Retries: 3, LogLevel: "info"},},{name: "自定义重试",opts: []Option{WithRetries(5)},want: &Config{Timeout: 5, Retries: 5, LogLevel: "info"},},{name: "全部自定义",opts: []Option{WithTimeout(10), WithRetries(0)},want: &Config{Timeout: 10, Retries: 0, LogLevel: "info"},},}for _, tt := range tests {t.Run(tt.name, func(t *testing.T) {got := NewConfig(tt.opts...)if got.Timeout != tt.want.Timeout {t.Errorf("Timeout: got %d, want %d", got.Timeout, tt.want.Timeout)}if got.Retries != tt.want.Retries {t.Errorf("Retries: got %d, want %d", got.Retries, tt.want.Retries)}if got.LogLevel != tt.want.LogLevel {t.Errorf("LogLevel: got %s, want %s", got.LogLevel, tt.want.LogLevel)}})} }
场景:为 HTTP API handler 编写单元测试,测试不同请求方法、路径和参数的响应。 答案:
go
运行
// api/handler.go package apiimport ("encoding/json""net/http" )// UserHandler 用户API处理器 func UserHandler(w http.ResponseWriter, r *http.Request) {switch r.Method {case http.MethodGet:handleGetUser(w, r)case http.MethodPost:handleCreateUser(w, r)default:w.WriteHeader(http.StatusMethodNotAllowed)json.NewEncoder(w).Encode(map[string]string{"error": "方法不允许"})} }func handleGetUser(w http.ResponseWriter, r *http.Request) {id := r.URL.Query().Get("id")if id == "" {w.WriteHeader(http.StatusBadRequest)json.NewEncoder(w).Encode(map[string]string{"error": "缺少id参数"})return}// 模拟查询用户w.WriteHeader(http.StatusOK)json.NewEncoder(w).Encode(map[string]string{"id": id, "name": "User " + id}) }func handleCreateUser(w http.ResponseWriter, r *http.Request) {var req struct{ Name string }if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Name == "" {w.WriteHeader(http.StatusBadRequest)json.NewEncoder(w).Encode(map[string]string{"error": "无效请求"})return}// 模拟创建用户w.WriteHeader(http.StatusCreated)json.NewEncoder(w).Encode(map[string]string{"id": "new123", "name": req.Name}) }// api/handler_test.go package apiimport ("bytes""encoding/json""net/http""net/http/httptest""testing" )func TestUserHandler(t *testing.T) {tests := []struct {name stringmethod stringpath stringbody stringwantStatus intwantBody map[string]string}{{name: "GET无id参数",method: http.MethodGet,path: "/user",wantStatus: http.StatusBadRequest,wantBody: map[string]string{"error": "缺少id参数"},},{name: "GET有id参数",method: http.MethodGet,path: "/user?id=123",wantStatus: http.StatusOK,wantBody: map[string]string{"id": "123", "name": "User 123"},},{name: "POST无效请求",method: http.MethodPost,path: "/user",body: `{"invalid": "field"}`,wantStatus: http.StatusBadRequest,wantBody: map[string]string{"error": "无效请求"},},{name: "POST创建用户",method: http.MethodPost,path: "/user",body: `{"Name": "Alice"}`,wantStatus: http.StatusCreated,wantBody: map[string]string{"id": "new123", "name": "Alice"},},{name: "不支持的方法",method: http.MethodPut,path: "/user",wantStatus: http.StatusMethodNotAllowed,wantBody: map[string]string{"error": "方法不允许"},},}for _, tt := range tests {t.Run(tt.name, func(t *testing.T) {// 创建请求var req *http.Requestif tt.body == "" {req = httptest.NewRequest(tt.method, tt.path, nil)} else {req = httptest.NewRequest(tt.method, tt.path, bytes.NewBufferString(tt.body))}req.Header.Set("Content-Type", "application/json")// 记录响应w := httptest.NewRecorder()// 调用handlerUserHandler(w, req)// 检查状态码resp := w.Result()if resp.StatusCode != tt.wantStatus {t.Errorf("状态码: got %d, want %d", resp.StatusCode, tt.wantStatus)}// 检查响应体var gotBody map[string]stringif err := json.NewDecoder(resp.Body).Decode(&gotBody); err != nil {t.Fatalf("解析响应体失败: %v", err)}for k, v := range tt.wantBody {if gotBody[k] != v {t.Errorf("响应字段 %s: got %s, want %s", k, gotBody[k], v)}}})} }
场景:使用
TestMain
实现测试前的数据库初始化和测试后的资源清理。 答案:go
运行
// db/db.go package dbimport ("database/sql"_ "github.com/mattn/go-sqlite3""os" )var DB *sql.DB// Init 初始化数据库连接 func Init(path string) error {var err errorDB, err = sql.Open("sqlite3", path)return err }// Close 关闭数据库连接 func Close() error {return DB.Close() }// db/db_test.go package dbimport ("os""testing" )const testDBPath = "test.db"// TestMain 在所有测试前初始化,测试后清理 func TestMain(m *testing.M) {// Setup: 初始化测试数据库if err := Init(testDBPath); err != nil {panic("初始化数据库失败: " + err.Error())}// 创建测试表_, err := DB.Exec(`CREATE TABLE IF NOT EXISTS test (id INT)`)if err != nil {panic("创建表失败: " + err.Error())}// 运行所有测试code := m.Run()// Teardown: 清理资源DB.Close()os.Remove(testDBPath) // 删除测试数据库文件os.Exit(code) }// 测试数据库查询 func TestQuery(t *testing.T) {// 插入测试数据_, err := DB.Exec(`INSERT INTO test (id) VALUES (1)`)if err != nil {t.Fatalf("插入数据失败: %v", err)}// 查询数据var id interr = DB.QueryRow(`SELECT id FROM test WHERE id = 1`).Scan(&id)if err != nil {t.Fatalf("查询失败: %v", err)}if id != 1 {t.Errorf("查询结果错误: got %d, want 1", id)} }
场景:编写模糊测试,测试一个字符串解析函数,自动生成输入并发现潜在问题。 答案:
go
运行
// parser/parser.go package parserimport "strconv"// ParseNumber 从字符串中提取数字并转换为整数 func ParseNumber(s string) (int, error) {// 简化实现:提取字符串中的第一个数字序列start := -1for i, c := range s {if c >= '0' && c <= '9' {if start == -1 {start = i}} else if start != -1 {return strconv.Atoi(s[start:i])}}if start != -1 {return strconv.Atoi(s[start:])}return 0, strconv.ErrSyntax }// parser/parser_test.go package parserimport ("testing" )// 常规测试 func TestParseNumber(t *testing.T) {tests := []struct {input stringwant interr bool}{{"123", 123, false},{"abc123def", 123, false},{"456", 456, false},{"no numbers", 0, true},}// 测试逻辑省略... }// 模糊测试:自动生成输入 func FuzzParseNumber(f *testing.F) {// 添加种子输入seedInputs := []string{"", "1", "123", "a1b2c3", " 456 ", "12.34", "-789",}for _, s := range seedInputs {f.Add(s)}// 模糊测试逻辑f.Fuzz(func(t *testing.T, input string) {_, err := ParseNumber(input)// 验证没有panic,或检查特定错误if err != nil {// 可以添加更多错误验证逻辑return}// 对于有效输入,可以添加更多验证}) }// 运行命令:go test -fuzz=. -fuzztime=10s // 模糊测试会尝试生成各种输入,可能发现如负数处理、空格等边界问题
高级应用(11-15 题)
场景:测试一个并发安全的计数器,使用
-race
检测竞态条件,并编写并发测试用例。 答案:go
运行
// counter/counter.go package counterimport "sync"// Counter 并发安全的计数器 type Counter struct {mu sync.Mutexvalue int }// Increment 增加计数 func (c *Counter) Increment() {c.mu.Lock()defer c.mu.Unlock()c.value++ }// Decrement 减少计数 func (c *Counter) Decrement() {c.mu.Lock()defer c.mu.Unlock()c.value-- }// Value 返回当前值 func (c *Counter) Value() int {c.mu.Lock()defer c.mu.Unlock()return c.value }// counter/counter_test.go package counterimport ("sync""testing" )// 基本功能测试 func TestCounter(t *testing.T) {c := &Counter{}if c.Value() != 0 {t.Errorf("初始值错误: got %d, want 0", c.Value())}c.Increment()if c.Value() != 1 {t.Errorf("Increment后值错误: got %d, want 1", c.Value())}c.Decrement()if c.Value() != 0 {t.Errorf("Decrement后值错误: got %d, want 0", c.Value())} }// 并发测试:检测竞态条件 func TestCounterConcurrent(t *testing.T) {c := &Counter{}var wg sync.WaitGroupnumGoroutines := 1000operationsPerGoroutine := 100// 启动多个goroutine并发操作for i := 0; i < numGoroutines; i++ {wg.Add(1)go func() {defer wg.Done()for j := 0; j < operationsPerGoroutine; j++ {c.Increment()}}()}wg.Wait()// 验证结果expected := numGoroutines * operationsPerGoroutineif c.Value() != expected {t.Errorf("并发计数错误: got %d, want %d", c.Value(), expected)} }// 运行命令:go test -race // 若实现中没有正确使用互斥锁,会检测到竞态条件
场景:使用 Testify 框架的
assert
和mock
包简化测试代码,测试一个依赖外部服务的函数。 答案:go
运行
// payment/service.go package payment// PaymentProcessor 支付处理器接口 type PaymentProcessor interface {Charge(amount float64, cardNumber string) (string, error) }// Service 支付服务 type Service struct {processor PaymentProcessor }func NewService(processor PaymentProcessor) *Service {return &Service{processor: processor} }// ProcessPayment 处理支付 func (s *Service) ProcessPayment(amount float64, card string) (string, error) {if amount <= 0 {return "", Errorf("无效金额: %v", amount)}return s.processor.Charge(amount, card) }// payment/service_test.go package paymentimport ("errors""testing""github.com/stretchr/testify/assert""github.com/stretchr/testify/mock" )// MockProcessor 模拟支付处理器 type MockProcessor struct {mock.Mock }func (m *MockProcessor) Charge(amount float64, cardNumber string) (string, error) {args := m.Called(amount, cardNumber)return args.String(0), args.Error(1) }func TestService_ProcessPayment(t *testing.T) {// 创建模拟处理器mockProcessor := new(MockProcessor)service := NewService(mockProcessor)t.Run("无效金额", func(t *testing.T) {_, err := service.ProcessPayment(-100, "4111-1111-1111-1111")assert.Error(t, err)assert.Contains(t, err.Error(), "无效金额")})t.Run("支付成功", func(t *testing.T) {// 设置模拟预期mockProcessor.On("Charge", 99.99, "4111-1111-1111-1111").Return("txn123", nil)// 调用服务txnID, err := service.ProcessPayment(99.99, "4111-1111-1111-1111")// 验证结果assert.NoError(t, err)assert.Equal(t, "txn123", txnID)mockProcessor.AssertExpectations(t) // 确保模拟方法被正确调用})t.Run("支付失败", func(t *testing.T) {mockProcessor.On("Charge", 50.0, "4111-1111-1111-1111").Return("", errors.New("卡已过期"))_, err := service.ProcessPayment(50.0, "4111-1111-1111-1111")assert.Error(t, err)assert.Equal(t, "卡已过期", err.Error())}) }
场景:为一个定时任务函数编写测试,使用
testing.Timer
控制时间,避免测试耗时过长。 答案:go
运行
// scheduler/scheduler.go package schedulerimport "time"// Task 定时任务 type Task struct {Interval time.DurationRun func() }// Start 启动定时任务 func (t *Task) Start(done chan struct{}) {ticker := time.NewTicker(t.Interval)defer ticker.Stop()for {select {case <-ticker.C:t.Run()case <-done:return}} }// scheduler/scheduler_test.go package schedulerimport ("testing""time" )func TestTask_Start(t *testing.T) {// 控制测试中的时间t.Parallel()// 记录任务执行次数count := 0task := &Task{Interval: 100 * time.Millisecond,Run: func() {count++},}done := make(chan struct{})go task.Start(done)// 使用定时器控制测试时长timer := time.NewTimer(350 * time.Millisecond)<-timer.Cclose(done)// 验证任务执行次数(预期3-4次)if count < 3 || count > 4 {t.Errorf("任务执行次数错误: got %d, want 3-4", count)} }// 更精确的测试:使用testing包的计时器(Go 1.14+) func TestTask_StartWithTimer(t *testing.T) {t.Parallel()count := 0task := &Task{Interval: 100 * time.Millisecond,Run: func() {count++},}done := make(chan struct{})go task.Start(done)// 使用testing的计时器控制时间timer := time.AfterFunc(350*time.Millisecond, func() {close(done)})defer timer.Stop()// 等待任务结束<-doneif count < 3 || count > 4 {t.Errorf("任务执行次数错误: got %d, want 3-4", count)} }
场景:测试一个文件处理函数,使用临时文件作为测试输入,避免依赖真实文件。 答案:
go
运行
// fileutil/fileutil.go package fileutilimport ("os""strings" )// CountLines 统计文件中非空行数量 func CountLines(path string) (int, error) {data, err := os.ReadFile(path)if err != nil {return 0, err}lines := strings.Split(string(data), "\n")count := 0for _, line := range lines {if strings.TrimSpace(line) != "" {count++}}return count, nil }// fileutil/fileutil_test.go package fileutilimport ("os""testing" )func TestCountLines(t *testing.T) {// 创建临时文件tempFile, err := os.CreateTemp("", "testfile*.txt")if err != nil {t.Fatalf("创建临时文件失败: %v", err)}defer os.Remove(tempFile.Name()) // 清理临时文件tests := []struct {name stringcontent stringwant intwantErr bool}{{"空文件", "", 0, false},{"单行文本", "hello", 1, false},{"多行文本", "line1\nline2\nline3", 3, false},{"包含空行", "line1\n\nline3", 2, false},{"包含空格行", " \nline2 \n ", 1, false},}for _, tt := range tests {t.Run(tt.name, func(t *testing.T) {// 写入测试内容if _, err := tempFile.WriteString(tt.content); err != nil {t.Fatalf("写入临时文件失败: %v", err)}// 确保内容被写入if err := tempFile.Sync(); err != nil {t.Fatalf("同步文件失败: %v", err)}// 重置文件指针(如需多次写入)if _, err := tempFile.Seek(0, 0); err != nil {t.Fatalf("移动文件指针失败: %v", err)}// 测试函数got, err := CountLines(tempFile.Name())if (err != nil) != tt.wantErr {t.Errorf("CountLines() error = %v, wantErr %v", err, tt.wantErr)return}if got != tt.want {t.Errorf("CountLines() = %v, want %v", got, tt.want)}})} }// 测试文件不存在的情况 func TestCountLines_FileNotFound(t *testing.T) {_, err := CountLines("nonexistent.txt")if err == nil {t.Error("预期错误,但未返回错误")return}if !os.IsNotExist(err) {t.Errorf("错误类型不正确: got %v, want file not found", err)} }
场景:实现一个测试套件,为复杂数据结构(如二叉树)的各种操作(插入、删除、查找)编写全面的测试。 答案:
go
运行
// tree/tree.go package tree// Node 二叉树节点 type Node struct {Val intLeft *NodeRight *Node }// BST 二叉搜索树 type BST struct {Root *Node }// Insert 插入值 func (t *BST) Insert(val int) {t.Root = insert(t.Root, val) }func insert(n *Node, val int) *Node {if n == nil {return &Node{Val: val}}if val < n.Val {n.Left = insert(n.Left, val)} else {n.Right = insert(n.Right, val)}return n }// Search 查找值 func (t *BST) Search(val int) bool {return search(t.Root, val) }func search(n *Node, val int) bool {if n == nil {return false}if val == n.Val {return true}if val < n.Val {return search(n.Left, val)}return search(n.Right, val) }// InOrder 中序遍历 func (t *BST) InOrder() []int {var res []intinOrder(t.Root, &res)return res }func inOrder(n *Node, res *[]int) {if n == nil {return}inOrder(n.Left, res)*res = append(*res, n.Val)inOrder(n.Right, res) }// tree/tree_test.go package treeimport ("testing" )// 测试套件:初始化测试树 func setupTree(values ...int) *BST {tree := &BST{}for _, v := range values {tree.Insert(v)}return tree }// 测试插入和中序遍历(应返回排序结果) func TestBST_InsertAndInOrder(t *testing.T) {tests := []struct {name stringinput []intwant []int}{{"空树", []int{}, []int{}},{"单元素", []int{5}, []int{5}},{"多元素", []int{3, 1, 4, 2}, []int{1, 2, 3, 4}},{"重复元素", []int{5, 3, 5, 7}, []int{3, 5, 5, 7}},}for _, tt := range tests {t.Run(tt.name, func(t *testing.T) {tree := setupTree(tt.input...)got := tree.InOrder()if !equalSlices(got, tt.want) {t.Errorf("InOrder() = %v, want %v", got, tt.want)}})} }// 测试查找功能 func TestBST_Search(t *testing.T) {tree := setupTree(5, 3, 7, 1, 9)tests := []struct {name stringval intwant bool}{{"存在的值", 3, true},{"存在的值", 9, true},{"不存在的值", 2, false},{"小于最小值", 0, false},{"大于最大值", 10, false},}for _, tt := range tests {t.Run(tt.name, func(t *testing.T) {got := tree.Search(tt.val)if got != tt.want {t.Errorf("Search(%d) = %v, want %v", tt.val, got, tt.want)}})} }// 辅助函数:比较两个切片是否相等 func equalSlices(a, b []int) bool {if len(a) != len(b) {return false}for i := range a {if a[i] != b[i] {return false}}return true }
总结
以上题目全面覆盖了 Go 语言单元测试的核心知识点:
八股文题从基础概念(测试文件命名、
testing
包使用)到高级特性(模糊测试、并发测试、第三方框架),解析了单元测试的设计原理及最佳实践。场景题结合实际开发场景(函数测试、HTTP handler 测试、数据库依赖测试、并发安全测试),展示了不同复杂度下的测试技巧。
通过练习这些题目,可深入理解 Go 语言单元测试的设计哲学,掌握编写可靠、高效测试代码的能力,从而提高软件质量和可维护性