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 题)
  1. 问题 :Go 语言的单元测试是什么?其主要目的是什么? 答案

    • 定义:单元测试是针对程序中最小可测试单元(如函数、方法)的测试,验证其在各种输入下的行为是否符合预期。

    • 主要目的:

      • 确保单个组件功能正确性。

      • 及早发现代码缺陷,降低修复成本。

      • 支持重构,保证修改后功能不受影响。

      • 作为代码文档,展示如何使用组件。

  2. 问题 :Go 单元测试的文件命名规则和函数命名规则是什么? 答案

    • 测试文件命名:必须以_test.go结尾(如math_test.go对应math.go)。

    • 测试函数命名:必须以Test开头,参数为*testing.T,格式为func TestXxx(t *testing.T)

    • 示例:func TestAdd(t *testing.T) { ... }

  3. 问题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...):跳过当前测试。

  4. 问题 :如何运行 Go 单元测试?go test命令的常用参数有哪些? 答案

    • 运行测试:在包目录执行go test,或指定包路径go test ./mypkg

    • 常用参数:

      • -v:显示详细测试输出。

      • -run 模式:只运行名称匹配模式的测试(如-run TestAdd)。

      • -cover:显示代码覆盖率。

      • -count n:指定测试运行次数(默认 1)。

      • -short:运行短测试(跳过耗时测试)。

  5. 问题 :什么是测试覆盖率?如何查看和分析 Go 代码的测试覆盖率? 答案

    • 定义:被测试覆盖的代码占总代码的比例,衡量测试的全面性。

    • 查看方法:

      • 基本覆盖率:go test -cover(输出百分比)。

      • 生成覆盖率报告:go test -coverprofile=cover.out

      • 可视化分析:go tool cover -html=cover.out(生成 HTML 报告)。

  6. 问题 :如何编写一个简单的单元测试?请举例说明测试函数的基本结构。 答案 : 示例:测试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)
                }
            })
        }
    }
  7. 问题 :子测试(subtest)的作用是什么?如何使用t.Run创建子测试? 答案

    • 作用:将一个测试函数拆分为多个相关子测试,便于单独运行和组织测试用例,失败时能精确定位。

    • 使用方法:通过t.Run(name, func(t *testing.T) { ... })创建,第一个参数为子测试名称,第二个为测试函数。

    • 示例:见上题中的t.Run(c.name, ...),可单独运行go test -run TestAdd/positive

  8. 问题t.Errort.Fatal的区别是什么?分别在什么场景下使用? 答案

    • 区别:

      • t.Error:记录错误信息,继续执行当前测试函数的后续代码。

      • t.Fatal:记录错误信息并调用t.FailNow(),立即终止当前测试函数(后续代码不执行)。

    • 场景:

      • t.Error:适合非致命错误,希望继续执行其他测试用例。

      • t.Fatal:适合致命错误(如初始化失败),继续执行无意义的场景。

中级篇(9-15 题)
  1. 问题 :什么是基准测试(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为动态调整的迭代次数
          }
      }
  2. 问题 :如何测试带有外部依赖(如数据库、网络)的函数?什么是测试替身? 答案

    • 测试方法:使用测试替身(Test Double)替代真实外部依赖,隔离被测试代码。

    • 测试替身类型:

      • 模拟(Mock):验证交互行为(如是否调用特定方法)。

      • 存根(Stub):返回预设值,不验证交互。

      • 假实现(Fake):简化的真实实现(如内存数据库)。

    • 示例:用接口定义依赖,测试时传入模拟实现。

  3. 问题table-driven test(表格驱动测试)的特点是什么?如何实现? 答案

    • 特点:将多个测试用例组织在切片中,通过循环执行,代码简洁、易扩展,新增用例只需添加表格行。

    • 实现步骤:

      1. 定义包含输入、预期输出和名称的结构体切片。

      2. 循环遍历切片,用子测试执行每个用例。

    • 示例:见第 6 题中的cases切片实现。

  4. 问题 :如何跳过某些测试?t.Skip-short flag 如何配合使用? 答案

    • 跳过测试:在测试函数中调用t.Skip("原因"),该测试会被标记为跳过。

    • 复制代码
      -short

      配合:通过

      复制代码
      testing.Short()

      判断是否启用短模式,跳过耗时测试:

      go

      运行

      复制代码
      func TestLongRunning(t *testing.T) {
          if testing.Short() {
              t.Skip("短模式下跳过耗时测试")
          }
          // 执行耗时测试...
      }

      运行:

      复制代码
      go test -short

      会跳过该测试。

  5. 问题 :如何测试错误返回?如何验证错误信息的正确性? 答案

    • 测试方法:检查函数返回的错误是否为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)
          }
      }
  6. 问题 :什么是模糊测试(fuzzing)?Go 如何支持模糊测试? 答案

    • 定义:自动生成大量随机输入测试函数,发现边界情况和潜在漏洞的测试方法。

    • Go 支持(1.18+):

      • 函数命名:func FuzzXxx(f *testing.F) { ... }

      • 步骤:添加种子输入(f.Add(...)),定义测试逻辑(f.Fuzz(func(t *testing.T, input 类型) { ... }))。

      • 运行:go test -fuzz=.

  7. 问题 :如何测试私有函数(未导出函数)?有哪些最佳实践? 答案

    • 测试方法:

      1. 在同一包内的测试文件中直接调用(推荐,因测试文件属于同一包)。

      2. 重构:将私有函数逻辑提取为导出函数(不推荐,破坏封装)。

    • 最佳实践:优先通过测试公共函数间接测试私有函数,必要时在同包测试文件中直接测试。

高级篇(16-20 题)
  1. 问题 :如何为 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)
          }
      }
  2. 问题 :测试中的 Setup 和 Teardown 如何实现?如何在多个测试间共享资源? 答案

    • 实现方式:

      1. 包级 Setup:在TestMain中执行,所有测试前运行 Setup,测试后运行 Teardown。

      2. 测试内 Setup:在测试函数中调用通用初始化函数。

    • 示例(

      复制代码
      TestMain

      ):

      go

      运行

      复制代码
      func TestMain(m *testing.M) {
          // Setup: 初始化资源(如数据库连接)
          setup()
          
          // 运行所有测试
          code := m.Run()
          
          // Teardown: 清理资源
          teardown()
          
          os.Exit(code)
      }
  3. 问题 :如何使用第三方测试框架(如 Testify)简化测试代码?与标准库相比有何优势? 答案

    • Testify 使用:提供assertrequire包简化断言,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")
      }
  4. 问题 :如何测试并发代码?有哪些工具和方法可以检测竞态条件? 答案

    • 测试方法:

      1. 使用-race flag 检测竞态条件:go test -race

      2. 编写并发测试用例,启动多个 goroutine 操作共享资源。

      3. 使用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())
          }
      }
  5. 问题 :单元测试的最佳实践有哪些?如何编写高质量的测试代码? 答案

    • 最佳实践:

      1. 测试应独立、可重复(不依赖外部状态)。

      2. 测试失败时提供清晰的错误信息。

      3. 覆盖边界条件(空输入、极值、错误情况)。

      4. 测试代码与生产代码保持同等质量。

      5. 避免测试实现细节(测试行为而非实现)。

      6. 定期运行测试,结合 CI/CD 自动化测试。

二、场景题(15 题)

基础应用(1-5 题)
  1. 场景 :为一个整数切片工具函数编写单元测试,包括求最大值、最小值和求和功能。 答案

    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 类似,省略实现
  2. 场景 :为一个字符串反转函数编写单元测试,覆盖普通字符串、空字符串、包含中文字符的情况。 答案

    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)
                }
            })
        }
    }
  3. 场景 :编写基准测试,比较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性能远优于+运算符,内存分配更少
  4. 场景 :使用子测试和表格驱动测试,测试一个简单的用户注册验证函数(检查用户名、密码合法性)。 答案

    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)
                }
            })
        }
    }
  5. 场景 :测试一个可能返回多种错误类型的函数,使用errors.Iserrors.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 题)
  1. 场景 :为一个依赖数据库的用户服务编写单元测试,使用接口和模拟对象隔离数据库依赖。 答案

    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
    }
  2. 场景 :编写测试覆盖一个带有默认参数的函数,测试不同参数组合的情况。 答案

    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)
                }
            })
        }
    }
  3. 场景 :为 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)
                    }
                }
            })
        }
    }
  4. 场景 :使用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)
        }
    }
  5. 场景 :编写模糊测试,测试一个字符串解析函数,自动生成输入并发现潜在问题。 答案

    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 题)
  1. 场景 :测试一个并发安全的计数器,使用-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
    // 若实现中没有正确使用互斥锁,会检测到竞态条件
  2. 场景 :使用 Testify 框架的assertmock包简化测试代码,测试一个依赖外部服务的函数。 答案

    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())
        })
    }
  3. 场景 :为一个定时任务函数编写测试,使用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)
        }
    }
  4. 场景 :测试一个文件处理函数,使用临时文件作为测试输入,避免依赖真实文件。 答案

    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)
        }
    }
  5. 场景 :实现一个测试套件,为复杂数据结构(如二叉树)的各种操作(插入、删除、查找)编写全面的测试。 答案

    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 语言单元测试的设计哲学,掌握编写可靠、高效测试代码的能力,从而提高软件质量和可维护性

相关推荐
资深web全栈开发3 小时前
并查集(Union-Find)套路详解
leetcode·golang·并查集·unionfind
moxiaoran57535 小时前
Go语言的递归函数
开发语言·后端·golang
朝花不迟暮5 小时前
Go基础-闭包
android·开发语言·golang
西京刀客7 小时前
go语言-切片排序之sort.Slice 和 sort.SliceStable 的区别(数据库分页、内存分页场景注意点)
后端·golang·sort·数据库分页·内存分页
黄昏单车8 小时前
golang语言基础到进阶学习笔记
笔记·golang·go
moxiaoran575318 小时前
Go语言结构体
开发语言·后端·golang
Tony Bai1 天前
Cloudflare 2025 年度报告发布——Go 语言再次“屠榜”API 领域,AI 流量激增!
开发语言·人工智能·后端·golang
小徐Chao努力1 天前
Go语言核心知识点底层原理教程【变量、类型与常量】
开发语言·后端·golang
锥锋骚年1 天前
go语言异常处理方案
开发语言·后端·golang
moxiaoran57531 天前
Go语言的map
开发语言·后端·golang