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

相关推荐
ALex_zry6 小时前
Golang云端编程入门指南:前沿框架与技术全景解析
开发语言·后端·golang
逢生博客1 天前
Ubuntu Server 快速部署长安链:基于 Go 的智能合约实现商品溯源
ubuntu·golang·区块链·智能合约·web3.0·长安链·商品溯源
澡点睡觉1 天前
【golang长途旅行第32站】反射
开发语言·后端·golang
007php0071 天前
使用 Docker、Jenkins、Harbor 和 GitLab 构建 CI/CD 流水线
数据库·ci/cd·docker·容器·golang·gitlab·jenkins
好学且牛逼的马1 天前
golang6 条件循环
golang
不过普通话一乙不改名2 天前
第四章:并发编程的基石与高级模式之Select语句与多路复用
开发语言·golang
Pure_Eyes2 天前
go 常见面试题
开发语言·后端·golang
{⌐■_■}2 天前
【ElasticSearch】使用docker compose,通过编写yml安装es8.15和kibana可视化界面操作,go连接es
elasticsearch·docker·golang
Tony Bai3 天前
泛型重塑 Go 错误检查:errors.As 的下一站 AsA?
开发语言·后端·golang