-
摘要
该视频主要讲述了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 mathutil func Add(a, b int) int { return a + b } // math_test.go package mathutil import "testing" func TestAdd(t *testing.T) { // 测试用例 cases := []struct { name string a, b int want 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() // 调用handler HelloHandler(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 Counter var wg sync.WaitGroup for 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 sliceutil import "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 := 0 for _, n := range nums { sum += n } return sum } // sliceutil/sliceutil_test.go package sliceutil import ( "errors" "testing" ) func TestMax(t *testing.T) { tests := []struct { name string input []int want int wantErr 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 strutil func 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 strutil import "testing" func TestReverse(t *testing.T) { cases := []struct { name string input string want 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 main import ( "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.Builder builder.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 user import "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 string func (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 user import ( "testing" ) func TestValidateRegistration(t *testing.T) { tests := []struct { name string username string password string wantErr bool errMsg 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 store import "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 store import ( "errors" "testing" ) func TestGetResource(t *testing.T) { tests := []struct { name string id string want string wantErr 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 string Name 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 user import ( "errors" "testing" ) // MockStore 模拟数据库实现 type MockStore struct { users map[string]*User err 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 string mockErr error users map[string]*User id string want *User wantErr 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 int Retries int LogLevel 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 config import "testing" func TestNewConfig(t *testing.T) { tests := []struct { name string opts []Option want *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 api import ( "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 api import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" ) func TestUserHandler(t *testing.T) { tests := []struct { name string method string path string body string wantStatus int wantBody 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.Request if 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() // 调用handler UserHandler(w, req) // 检查状态码 resp := w.Result() if resp.StatusCode != tt.wantStatus { t.Errorf("状态码: got %d, want %d", resp.StatusCode, tt.wantStatus) } // 检查响应体 var gotBody map[string]string if 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 db import ( "database/sql" _ "github.com/mattn/go-sqlite3" "os" ) var DB *sql.DB // Init 初始化数据库连接 func Init(path string) error { var err error DB, err = sql.Open("sqlite3", path) return err } // Close 关闭数据库连接 func Close() error { return DB.Close() } // db/db_test.go package db import ( "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 int err = 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 parser import "strconv" // ParseNumber 从字符串中提取数字并转换为整数 func ParseNumber(s string) (int, error) { // 简化实现:提取字符串中的第一个数字序列 start := -1 for 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 parser import ( "testing" ) // 常规测试 func TestParseNumber(t *testing.T) { tests := []struct { input string want int err 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 counter import "sync" // Counter 并发安全的计数器 type Counter struct { mu sync.Mutex value 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 counter import ( "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.WaitGroup numGoroutines := 1000 operationsPerGoroutine := 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 * operationsPerGoroutine if 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 payment import ( "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 scheduler import "time" // Task 定时任务 type Task struct { Interval time.Duration Run 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 scheduler import ( "testing" "time" ) func TestTask_Start(t *testing.T) { // 控制测试中的时间 t.Parallel() // 记录任务执行次数 count := 0 task := &Task{ Interval: 100 * time.Millisecond, Run: func() { count++ }, } done := make(chan struct{}) go task.Start(done) // 使用定时器控制测试时长 timer := time.NewTimer(350 * time.Millisecond) <-timer.C close(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 := 0 task := &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() // 等待任务结束 <-done if count < 3 || count > 4 { t.Errorf("任务执行次数错误: got %d, want 3-4", count) } }
-
场景 :测试一个文件处理函数,使用临时文件作为测试输入,避免依赖真实文件。 答案:
go
运行
// fileutil/fileutil.go package fileutil import ( "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 := 0 for _, line := range lines { if strings.TrimSpace(line) != "" { count++ } } return count, nil } // fileutil/fileutil_test.go package fileutil import ( "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 string content string want int wantErr 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 int Left *Node Right *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 []int inOrder(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 tree import ( "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 string input []int want []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 string val int want 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 语言单元测试的设计哲学,掌握编写可靠、高效测试代码的能力,从而提高软件质量和可维护性