找回密码
 立即注册
首页 业界区 业界 单元测试(go)

单元测试(go)

挡缭 14 小时前
项目demo地址:go-test
目前只描述了简单的方法,文档持续更新中...
本文主要针对golang语言的单元测试工具,博客内容也会涉及一些单元相关的内容
什么是单元测试:单元测试是软件测试体系中最基础、最核心的测试类型,它聚焦于对软件系统中最小的 “可测试单元” 进行独立验证,确保该单元的功能符合预期设计。
简单描述下前因后果:工作需要对项目代码系统化执行单元测试,要求覆盖率达到95%以上,因为不同人的开发风格和代码习惯,外加项目框架和架构的一些要求。单元测试,这个东西一般情况都会让人很痛苦,至于因为啥,我相信看到我这篇博客的各位,都是有不同程度的感同身受的,我这里介绍三种单测工具,基础的单元测试书写和使用就不过多赘述了。
一、单元测试的核心方式

注意:这块作为扩展,如需直接了解工具可忽略这部分
单元测试的实现方式可从核心分类维度展开,结合具体落地实践和技术选型,不同方式适用于不同场景和项目规模。
1、按测试编写时机划分

(核心流程维度)
从开发流程角度区分的两种核心方式,决定了单元测试与业务代码的协作关系。
1.后写单元测试

(传统方式,最常用)

  • 核心定义:先编写业务功能代码,待功能实现完成后,再针对性补写对应的单元测试用例,验证已实现的代码逻辑是否符合预期。
  • 适用场景:大部分传统开发场景、快速迭代的小型需求、开发者对 TDD 模式不熟悉的项目。
  • 优势:符合开发者 “先实现功能再验证” 的直觉,上手成本低,无需提前设计详细的测试用例。
  • 劣势:可能遗漏部分边界场景的测试,且容易因业务代码耦合度高,导致测试难以编写(后期补测时,修改代码解耦的成本更高)。
2.测试驱动开发

(TDD,Test-Driven Development,进阶方式)

  • 核心定义:遵循 “先写测试,再写业务代码,最后重构” 的循环流程,测试用例先定义好被测单元的预期行为(输入、输出、异常场景),再编写满足测试用例的业务代码,最终优化代码结构。
  • 核心流程(红 - 绿 - 重构循环)

    • 红(Red):编写一个失败的测试用例(此时业务代码未实现,测试必然失败);
    • 绿(Green):编写最少的业务代码,仅满足让该测试用例通过(不追求代码优雅,只保证功能达标);
    • 重构(Refactor):在测试用例保驾护航的前提下,优化业务代码的结构、可读性、性能等,确保重构后测试用例仍能通过。

  • 适用场景:对代码质量要求高的核心模块、复杂业务逻辑、需要长期维护的大型项目。
  • 优势

    • 强制开发者提前梳理需求和接口设计,减少后期需求偏差;
    • 测试用例覆盖率更高,天然覆盖正常、边界、异常场景;
    • 重构无风险,测试用例作为 “安全网”,确保重构不破坏原有功能;
    • 代码耦合度更低,因为先写测试会倒逼开发者设计可测试的代码(如依赖接口而非具体实现)。

2、按依赖处理方式划分

(技术实现维度)
这是单元测试落地的核心技术维度,决定了如何隔离外部依赖,保证测试的独立性。
1. 基于 Mock/Stub 的单元测试

(主流方式)

  • 核心定义:当被测单元依赖外部资源(数据库、RPC 服务、HTTP 接口、文件系统等)时,通过 ** 模拟(Mock)桩(Stub)** 实现替代真实依赖,预设返回值或行为,从而脱离外部环境限制,专注测试业务逻辑。
  • Mock vs Stub 区别(通俗理解)
    类型核心特征适用场景Stub仅预设固定返回值,无行为验证只需依赖返回值完成业务逻辑测试Mock不仅预设返回值,还可验证依赖方法是否被调用、调用次数、参数是否正确需要验证业务逻辑对依赖的调用行为
  • 实现方式

    • 手动编写 Mock/Stub(简单场景):如之前 Go 示例中,手动实现UserDB接口的MockUserDB,预设返回值;
    • 工具自动生成 Mock(复杂场景):Go 生态的gomock+mockgen、Java 生态的Mockito、Python 生态的unittest.mock,可根据接口自动生成 Mock 代码,支持更灵活的行为验证。

  • 优势:测试独立、快速、可复现,不受外部资源状态影响,能覆盖各种异常场景(如依赖服务报错、超时)。
  • Go 语言工具示例(gomock)
    (1)先安装工具:
    1. go get github.com/golang/mock/gomock
    复制代码
    1. go install github.com/golang/mock/mockgen@latest
    复制代码
    (2)自动生成 Mock 代码,无需手动编写,支持验证调用行为。
2. 真实依赖单元测试

(小众场景)

  • 核心定义:不使用模拟实现,直接使用真实的外部依赖(如真实数据库、真实 HTTP 服务)进行单元测试,验证被测单元与真实依赖的协作是否正常。
  • 适用场景:依赖逻辑简单、真实依赖易于搭建和控制(如本地轻量数据库 SQLite)、对依赖协作正确性要求极高的场景。
  • 优势:测试结果更贴近生产环境,能发现与真实依赖协作的潜在问题(如 SQL 语法错误、接口参数不匹配)。
  • 劣势

    • 测试执行速度慢,依赖外部资源启动和初始化;
    • 测试结果不稳定,受外部资源状态影响(如数据库数据被修改导致测试失败);
    • 测试环境搭建复杂,需要统一管理依赖配置(如数据库连接信息、服务地址)。

  • 示例:测试数据库查询函数时,直接连接本地测试 MySQL 数据库,预先插入测试数据,执行查询后验证结果,最后清理测试数据。
3、个人理解

上面的描述是大模型系统化生成的内容,下面是博主自行整理的,至于为什么会有这样一段赘述,是和下面的工具有些关联
单元测试两种开发方式:

1.方式一

先开发业务代码,后写单元测试代码(常用)

  • interface单元测试

    • 核心优势:

      • 完全解耦外部依赖,实现 “纯净” 测试
      • 灵活覆盖全量业务场景,无测试死角
      • 测试执行效率极高,支持高频执行
      • 为代码重构提供 “安全网”,降低重构风险
      • 倒逼良好的代码设计,提升代码质量
      • 可验证依赖方法的调用行为(进阶优势)

    • 缺点:

      • 增加前期开发成本,引入额外代码量
      • 存在 “过度抽象” 的风险
      • 无法验证真实依赖的协作正确性
      • Mock 与真实实现可能存在 “行为不一致”
      • 对简单场景 “杀鸡用牛刀”,性价比不高

    • 总结:
      如果代码开发的时候考虑到需要进行单元测试功能开发,可以直接在业务功能开发时进行单元测试的预先埋点处理,做好接口的开发,不过一般情况下大家的开发习惯都不会考虑单元测试这种情况,这时候在想要回去处理单元测试,interface这种方式就会极为麻烦和笨重,单测时间成本成指数级增长。

  • 使用单元测试工具

    • 内置核心工具:testing包(基础基石)
    • 工具包(具体使用方法和功能下面介绍)

      • 接口测试工具:httptest
      • 数据库测试工具:go-sqlmock
      • 打桩测试工具:gomonkey

    • 优点:
      在业务逻辑代码开发完成后几乎可以不调整原始逻辑代码进行单元测试代码开发

2.方式二

先开发单元测试代码,后写逻辑代码(很少见,不介绍)
二、单元测试工具

主要介绍三个工具:httptest、go-sqlmock、gomonkey
1、httptest

介绍:Go 内置标准库net/http/httptest,核心用途用于测试net/http构建的HTTP服务(如API接口、Web服务等),它可以模拟HTTP请求发送和HTTP响应的接收,无需启动真实的HTTP服务器即可完成接口测试,极大提升了测试的便捷性和执行效率
优点:

  • 无需启动真实服务器:无需调用 http.ListenAndServe 启动端口监听,直接测试 HTTP 处理器(Handler/HandlerFunc),测试执行更高效。
  • 脱离网络依赖:模拟 HTTP 请求与响应的完整生命周期,不受网络波动、端口占用等外部因素影响,测试结果稳定可复现。
  • 精准捕获响应细节:可完整获取响应状态码、响应头、响应体等所有信息,便于精准断言验证。
  • 支持两种核心测试场景:

    • 测试 HTTP 处理器(直接调用 ServeHTTP,最常用)
    • 启动临时测试服务器(模拟真实服务端,用于客户端测试或集成测试)

1.安装

内置工具可以直接使用
2.使用示例

相关代码在gitee代码仓库的示例代码中,仓库地址请看博客开头
blog.go
  1. package blog
  2. import (
  3.         "errors"
  4.         "fmt"
  5.         "io"
  6.         "net/http"
  7. )
  8. func SearchHttp(targetURL string) (interface{}, error) {
  9.         resp, err := http.Get(targetURL)
  10.         if err != nil {
  11.                 errMsg := fmt.Sprintf("发送 GET 请求失败:%v", err)
  12.                 fmt.Println(errMsg)
  13.                 return nil, errors.New(errMsg)
  14.         }
  15.         defer resp.Body.Close()
  16.         if resp.StatusCode != http.StatusOK {
  17.                 errMsg := fmt.Sprintf("请求失败,状态码:%d,状态信息:%s", resp.StatusCode, resp.Status)
  18.                 fmt.Println(errMsg)
  19.                 return nil, errors.New(errMsg)
  20.         }
  21.         bodyBytes, _ := io.ReadAll(resp.Body)
  22.         return string(bodyBytes), nil
  23. }
复制代码
blog_test.go
  1. package blog
  2. import (
  3.         "net/http"
  4.         "net/http/httptest"
  5.         "strings"
  6.         "testing"
  7. )
  8. func TestSearchHttp(t *testing.T) {
  9.         // --------------- 1. 定义所有测试场景的表驱动用例(循环遍历执行) ---------------
  10.         testCases := []struct {
  11.                 name             string        // 用例名称
  12.                 prepareFunc      func() string // 前置准备:创建模拟服务器/构造URL,返回待请求的URL
  13.                 expectedErr      bool          // 是否预期返回错误
  14.                 errContains      string        // 预期错误信息包含的关键字(非空则验证)
  15.                 expectedNonEmpty bool          // 正常场景下,是否预期返回非空字符串
  16.         }{
  17.                 // 场景1:正常请求(200 OK,响应体正常)
  18.                 {
  19.                         name: "正常请求-状态码200",
  20.                         prepareFunc: func() string {
  21.                                 // 启动模拟HTTP服务器,返回200和测试响应体
  22.                                 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  23.                                         mockBody := "<!DOCTYPE html><html><title>百度一下</title></html>"
  24.                                         w.WriteHeader(http.StatusOK)
  25.                                         _, _ = w.Write([]byte(mockBody))
  26.                                 }))
  27.                                 // 关键:将mockServer放入测试上下文,确保后续能关闭(避免资源泄露)
  28.                                 t.Cleanup(func() { mockServer.Close() })
  29.                                 return mockServer.URL
  30.                         },
  31.                         expectedErr:      false,
  32.                         errContains:      "",
  33.                         expectedNonEmpty: true,
  34.                 },
  35.                 // 场景2:请求失败(无效URL,模拟网络异常)
  36.                 {
  37.                         name: "异常场景-无效URL请求失败",
  38.                         prepareFunc: func() string {
  39.                                 // 返回一个无效的URL,触发http.Get请求失败
  40.                                 return "http://invalid-xxx-url-12345/"
  41.                         },
  42.                         expectedErr:      true,
  43.                         errContains:      "发送 GET 请求失败",
  44.                         expectedNonEmpty: false,
  45.                 },
  46.                 // 场景3:状态码非200(模拟404 Not Found)
  47.                 {
  48.                         name: "异常场景-状态码404",
  49.                         prepareFunc: func() string {
  50.                                 // 启动模拟HTTP服务器,返回404状态码
  51.                                 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  52.                                         w.WriteHeader(http.StatusNotFound)
  53.                                         _, _ = w.Write([]byte("页面不存在"))
  54.                                 }))
  55.                                 t.Cleanup(func() { mockServer.Close() })
  56.                                 return mockServer.URL
  57.                         },
  58.                         expectedErr:      true,
  59.                         errContains:      "请求失败,状态码:404",
  60.                         expectedNonEmpty: false,
  61.                 },
  62.                 // 场景4:状态码非200(模拟500服务器内部错误)
  63.                 {
  64.                         name: "异常场景-状态码500",
  65.                         prepareFunc: func() string {
  66.                                 // 启动模拟HTTP服务器,返回500状态码
  67.                                 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  68.                                         w.WriteHeader(http.StatusInternalServerError)
  69.                                         _, _ = w.Write([]byte("服务器内部错误"))
  70.                                 }))
  71.                                 t.Cleanup(func() { mockServer.Close() })
  72.                                 return mockServer.URL
  73.                         },
  74.                         expectedErr:      true,
  75.                         errContains:      "请求失败,状态码:500",
  76.                         expectedNonEmpty: false,
  77.                 },
  78.         }
  79.         // --------------- 2. 循环遍历所有测试用例,统一执行验证 ---------------
  80.         for _, tc := range testCases {
  81.                 // 循环内使用t.Run创建子用例(便于精准定位失败场景,不影响其他用例)
  82.                 t.Run(tc.name, func(t *testing.T) {
  83.                         // 步骤1:执行前置准备,获取待请求的URL
  84.                         targetURL := tc.prepareFunc()
  85.                         // 步骤2:调用被测函数
  86.                         result, err := SearchHttp(targetURL)
  87.                         // 步骤3:统一断言验证
  88.                         // 3.1 验证错误是否符合预期
  89.                         if (err != nil) != tc.expectedErr {
  90.                                 t.Fatalf("错误预期不符:预期是否错误[%t],实际是否错误[%t],错误信息[%v]",
  91.                                         tc.expectedErr, err != nil, err)
  92.                         }
  93.                         // 3.2 若预期错误,验证错误信息是否包含指定关键字
  94.                         if tc.expectedErr && tc.errContains != "" {
  95.                                 if !strings.Contains(err.Error(), tc.errContains) {
  96.                                         t.Errorf("错误信息不符:预期包含[%s],实际错误[%v]", tc.errContains, err)
  97.                                 }
  98.                         }
  99.                         // 3.3 验证返回值是否符合预期
  100.                         if tc.expectedErr {
  101.                                 // 异常场景:预期返回nil
  102.                                 if result != nil {
  103.                                         t.Errorf("异常场景预期返回nil,实际返回[%v],类型[%T]", result, result)
  104.                                 }
  105.                         } else {
  106.                                 // 正常场景:验证返回值是string类型,且非空(若预期非空)
  107.                                 resultStr, ok := result.(string)
  108.                                 if !ok {
  109.                                         t.Fatalf("正常场景预期返回string类型,实际返回[%T]", result)
  110.                                 }
  111.                                 if tc.expectedNonEmpty && len(resultStr) == 0 {
  112.                                         t.Error("正常场景预期返回非空字符串,实际返回空字符串")
  113.                                 }
  114.                         }
  115.                 })
  116.         }
  117. }
复制代码
命令行执行命令
go test -cover
结果:
  1. PS D:\wyl\workspace\go\tracer\dao\blog> go test -cover
  2. 发送 GET 请求失败:Get "http://invalid-xxx-url-12345/": dial tcp: lookup invalid-xxx-url-12345: no such host
  3. 请求失败,状态码:404,状态信息:404 Not Found
  4. 请求失败,状态码:500,状态信息:500 Internal Server Error
  5. PASS
  6. coverage: 100.0% of statements
  7. ok      tracer/dao/blog 1.695s
  8. PS D:\wyl\workspace\go\tracer\dao\blog>
复制代码
2、go-sqlmock

介绍:gosqlmock是一个用于模拟数据库 /sql 驱动的库,核心作用是在不依赖真实数据库实例的情况下,对数据库相关逻辑进行单元测试,避免测试过程中操作真实数据、产生脏数据或依赖数据库服务可用性。
优点:

  • 解除真实数据库依赖,保证测试独立、稳定、无脏数据
  • 精准控制数据库行为,覆盖常规 / 异常全量测试场景
  • 兼容 database/sql 标准库和主流 ORM,无侵入式集成
  • 严格验证预期行为,提升测试准确性,发现隐藏问题
  • 轻量级无冗余,内存级执行,测试性能优异
  • 支持正则匹配,灵活适配复杂 SQL 场景
1.安装
  1. go get github.com/DATA-DOG/go-sqlmock
复制代码
2.使用示例

相关代码在gitee代码仓库的示例代码中,仓库地址请看博客开头
price_policy.go
  1. package price
  2. import (
  3.         "gorm.io/gorm"
  4.         "tracer/model"
  5. )
  6. type PricePolicy struct {
  7.         gorm.Model
  8.         Catogory      string `gorm:"type:varchar(64)" json:"catogory" label:"收费类型"`
  9.         Title         string `gorm:"type:varchar(64)" json:"title" label:"标题"`
  10.         Price         uint64 `gorm:"type:int(5)" json:"price" label:"价格"`
  11.         ProjectNum    uint64 `json:"project_num" label:"项目数量"`
  12.         ProjectMember uint64 `json:"project_member" label:"项目成员人数"`
  13.         ProjectSpace  uint64 `json:"project_space" label:"每个项目空间" help_text:"单位是M"`
  14.         PerFileSize   uint64 `json:"per_file_size" label:"单文件大小" help_text:"单位是M"`
  15. }
  16. // GetAllBlog 查询所有博客信息
  17. func GetAllBlog() PricePolicy {
  18.         var allBlog PricePolicy
  19.         model.DB.Find(&allBlog)
  20.         return allBlog
  21. }
  22. // TypeBlog 根据类型查找博客
  23. func TypeBlog(tyb string) PricePolicy {
  24.         var typeBlog PricePolicy
  25.         model.DB.Where("type=?", tyb).Find(&typeBlog)
  26.         return typeBlog
  27. }
  28. // TopBlog 置顶博客查询
  29. func TopBlog(top string) PricePolicy {
  30.         var topBlog PricePolicy
  31.         model.DB.Where("top=?", top).Find(&topBlog)
  32.         return topBlog
  33. }
复制代码
price_policy_test.go
  1. package price
  2. import (
  3.         "github.com/DATA-DOG/go-sqlmock"
  4.         "github.com/stretchr/testify/assert"
  5.         "gorm.io/driver/mysql"
  6.         "gorm.io/gorm"
  7.         "testing"
  8.         "time"
  9.         "tracer/model"
  10. )
  11. // TestGetAllBlog GetAllBlog 函数单元测试
  12. func TestGetAllBlog(t *testing.T) {
  13.         // 步骤1:创建 sqlmock 模拟连接(内存级,无真实数据库依赖)
  14.         // sqlmock.New() 返回 mockDB(*sql.DB)、mock(sqlmock.Sqlmock)、error
  15.         mockSqlDB, mock, err := sqlmock.New()
  16.         assert.NoError(t, err, "创建 sqlmock 连接失败")
  17.         defer mockSqlDB.Close() // 测试结束关闭模拟连接
  18.         // 步骤2:将 sqlmock 连接适配为 GORM 可用的 DB 实例
  19.         // 关键:使用 gorm mysql 驱动,传入 mock 的 *sql.DB 实例
  20.         gormDB, err := gorm.Open(mysql.New(mysql.Config{
  21.                 Conn:                      mockSqlDB, // 绑定 sqlmock 的连接
  22.                 SkipInitializeWithVersion: true,      // 跳过 MySQL 版本检测(模拟连接无需版本信息)
  23.         }), &gorm.Config{})
  24.         assert.NoError(t, err, "GORM 绑定 sqlmock 连接失败")
  25.         // 步骤3:替换全局 DB 为 mock 的 GORM DB(核心:让业务函数使用 mock 连接)
  26.         model.DB = gormDB
  27.         // 步骤4:构造模拟返回数据(与 PricePolicy 字段对应,需包含 gorm.Model 的默认字段)
  28.         expectedPolicy := PricePolicy{
  29.                 Model: gorm.Model{
  30.                         ID:        1,
  31.                         CreatedAt: time.Time{}, // 测试中可忽略时间字段,若需精确匹配可赋值 time.Time 实例
  32.                         UpdatedAt: time.Time{},
  33.                         DeletedAt: gorm.DeletedAt{},
  34.                 },
  35.                 Catogory:      "个人版",
  36.                 Title:         "基础收费套餐",
  37.                 Price:         99,
  38.                 ProjectNum:    5,
  39.                 ProjectMember: 10,
  40.                 ProjectSpace:  1024,
  41.                 PerFileSize:   50,
  42.         }
  43.         // 步骤5:设置 sqlmock 预期(关键:匹配 GORM 自动生成的 SQL 语句)
  44.         // GORM 的 Find(&allBlog) 会生成 SELECT * FROM `price_policies` 语句(表名默认是结构体小写复数)
  45.         // 使用正则匹配,忽略无关空格和潜在的字段顺序差异
  46.         rows := sqlmock.NewRows([]string{
  47.                 "id", "created_at", "updated_at", "deleted_at",
  48.                 "catogory", "title", "price", "project_num",
  49.                 "project_member", "project_space", "per_file_size",
  50.         }).AddRow(
  51.                 expectedPolicy.ID, expectedPolicy.CreatedAt, expectedPolicy.UpdatedAt, expectedPolicy.DeletedAt,
  52.                 expectedPolicy.Catogory, expectedPolicy.Title, expectedPolicy.Price, expectedPolicy.ProjectNum,
  53.                 expectedPolicy.ProjectMember, expectedPolicy.ProjectSpace, expectedPolicy.PerFileSize,
  54.         )
  55.         // 预设查询预期:匹配 GORM 生成的 SELECT 语句
  56.         mock.ExpectQuery("^SELECT \\* FROM `price_policies`").
  57.                 WillReturnRows(rows) // 设置查询返回的模拟数据
  58.         // 步骤6:执行待测试函数
  59.         actualPolicy := GetAllBlog()
  60.         // 步骤7:验证结果
  61.         // 验证核心字段是否一致(gorm.Model 时间字段若未赋值,可忽略或单独校验)
  62.         assert.Equal(t, expectedPolicy.ID, actualPolicy.ID, "ID 不匹配")
  63.         assert.Equal(t, expectedPolicy.Catogory, actualPolicy.Catogory, "收费类型不匹配")
  64.         assert.Equal(t, expectedPolicy.Title, actualPolicy.Title, "标题不匹配")
  65.         assert.Equal(t, expectedPolicy.Price, actualPolicy.Price, "价格不匹配")
  66.         assert.Equal(t, expectedPolicy.ProjectNum, actualPolicy.ProjectNum, "项目数量不匹配")
  67.         assert.Equal(t, expectedPolicy.ProjectMember, actualPolicy.ProjectMember, "项目成员人数不匹配")
  68.         assert.Equal(t, expectedPolicy.ProjectSpace, actualPolicy.ProjectSpace, "项目空间不匹配")
  69.         assert.Equal(t, expectedPolicy.PerFileSize, actualPolicy.PerFileSize, "单文件大小不匹配")
  70.         // 关键:验证所有 sqlmock 预期都已被执行(无遗漏、无多余操作)
  71.         assert.NoError(t, mock.ExpectationsWereMet(), "存在未满足的 sqlmock 预期")
  72. }
复制代码
命令行执行命令
go test -cover
结果:
  1. PS D:\wyl\workspace\go\tracer\model\price> go test -cover
  2. PASS
  3. coverage: 33.3% of statements
  4. ok      tracer/model/price      0.076s
复制代码
3、gomonkey

介绍:gomonkey是一款强大的运行时打桩(Mock)工具/动态 Mock 工具,能够在不修改源代码的前提下,对函数、方法、全局变量等进行动态替换,广泛用于单元测试场景
优点:

  • 无侵入式打桩,无需修改业务代码
  • 功能全面,支持函数、方法、全局变量等多种打桩场景
  • 支持私有成员打桩,适配遗留项目
  • 轻量级易用,API 简洁,兼容主流框架
  • 灵活控制打桩生命周期,精准适配测试需求
  • x86_64 (Intel/AMD) 架构下,功能基本完整、稳定,是生产级 Mock 工具
致命缺陷:

  • 对Windows、Mac、arm架构系统支持很不友好

    • 在 ARM64 (aarch64) 架构下,存在 大量核心功能失效、运行时崩溃、Mock 无效果 的缺陷
    • 缺陷是 gomonkey 的底层实现原理 导致的,而非 Go 语言 / ARM64 的兼容性问题,官方至今未彻底修复

  • Go1.18 + 版本 中更严重,Go1.17 及以下 ARM64 版本问题稍少,但依然存在关键缺陷
1.安装
  1. go get github.com/agiledragon/gomonkey/v2
复制代码
2.使用示例

(1)函数打桩方法

gomonkey.ApplyFunc()
相关代码在gitee代码仓库的示例代码中,仓库地址请看博客开头
参数示例:
  1. // 第一个参数:函数名
  2. // 第二个参数:打桩函数,入参和出参要保持和被打桩函数保持一致
  3. func ApplyFunc(target, double interface{}) *Patches {
  4.         return create().ApplyFunc(target, double)
  5. }
复制代码
blog.go
  1. package user
  2. import "tracer/model/user"
  3. // UserInfoDao 查询所有用户信息
  4. func UserInfoDao() (interface{}, error) {
  5.         obj, err := user.GetAllUser()
  6.         if err != nil {
  7.                 return nil, err
  8.         }
  9.         return obj, nil
  10. }
复制代码
blog_test.go
  1. package user
  2. import (
  3.         "errors"
  4.         "fmt"
  5.         "github.com/agiledragon/gomonkey/v2"
  6.         "gorm.io/gorm"
  7.         "testing"
  8.         "tracer/dao"
  9.         "tracer/model/user"
  10. )
  11. // TestUserInfoDao_AllScenarios 单个函数通过循环覆盖所有测试场景
  12. func TestUserInfoDao_AllScenarios(t *testing.T) {
  13.         // 1. 定义测试用例结构体:封装输入(打桩参数)和预期输出
  14.         type testCase struct {
  15.                 name          string          // 用例名称,便于排查错误
  16.                 mockUsers     []user.UserInfo // 打桩 GetAllUser 返回的用户列表
  17.                 mockErr       error           // 打桩 GetAllUser 返回的错误
  18.                 expectedErr   error           // 预期 UserInfoDao 返回的错误
  19.                 expectedNil   bool            // 预期 UserInfoDao 返回的数据是否为 nil
  20.                 expectedCount int             // 预期返回的用户数量(正常场景有效)
  21.         }
  22.         // 2. 构造所有测试用例(正常场景 + 异常场景)
  23.         testCases := []testCase{
  24.                 {
  25.                         name: "正常场景-返回2个用户",
  26.                         mockUsers: []user.UserInfo{
  27.                                 {
  28.                                         Model:    gorm.Model{ID: 1},
  29.                                         UserName: "zhangsan",
  30.                                         Password: "123456",
  31.                                         Phone:    "13800138000",
  32.                                         Email:    "zhangsan@test.com",
  33.                                 },
  34.                                 {
  35.                                         Model:    gorm.Model{ID: 2},
  36.                                         UserName: "lisi",
  37.                                         Password: "654321",
  38.                                         Phone:    "13900139000",
  39.                                         Email:    "lisi@test.com",
  40.                                 },
  41.                         },
  42.                         mockErr:       nil,
  43.                         expectedErr:   nil,
  44.                         expectedNil:   false,
  45.                         expectedCount: 2,
  46.                 },
  47.                 {
  48.                         name:          "正常场景-返回空用户列表",
  49.                         mockUsers:     []user.UserInfo{},
  50.                         mockErr:       nil,
  51.                         expectedErr:   nil,
  52.                         expectedNil:   false,
  53.                         expectedCount: 0,
  54.                 },
  55.                 {
  56.                         name:          "异常场景-GORM记录不存在错误",
  57.                         mockUsers:     nil,
  58.                         mockErr:       gorm.ErrRecordNotFound,
  59.                         expectedErr:   gorm.ErrRecordNotFound,
  60.                         expectedNil:   true,
  61.                         expectedCount: 0,
  62.                 },
  63.                 {
  64.                         name:          "异常场景-自定义查询错误",
  65.                         mockUsers:     nil,
  66.                         mockErr:       errors.New("数据库连接超时"),
  67.                         expectedErr:   errors.New("数据库连接超时"),
  68.                         expectedNil:   true,
  69.                         expectedCount: 0,
  70.                 },
  71.         }
  72.         // 3. 循环执行所有测试用例
  73.         for _, tc := range testCases {
  74.                 dao.InitDb()
  75.                 // t.Run:为每个用例创建独立的测试上下文,互不干扰,便于定位用例错误
  76.                 t.Run(tc.name, func(t *testing.T) {
  77.                         // 步骤1:对 GetAllUser 进行动态打桩(每个用例独立打桩,避免相互影响)
  78.                         // 使用ApplyFunc打桩跨包函数
  79.                         patches := gomonkey.ApplyFunc(user.GetAllUser, func() ([]user.UserInfo, error) {
  80.                                 // 返回当前用例预设的模拟数据和错误
  81.                                 return tc.mockUsers, tc.mockErr
  82.                         })
  83.                         defer patches.Reset() // 每个用例执行完毕后重置打桩,避免污染其他用例
  84.                         // 步骤2:执行待测试函数 UserInfoDao
  85.                         _, err := UserInfoDao()
  86.                         if err != nil {
  87.                                 fmt.Println(err)
  88.                         }
  89.                 })
  90.         }
  91. }
复制代码
命令行执行命令
go test -cover
结果:
  1. PS D:\wyl\workspace\go\tracer\dao\user> go test -cover
  2. PASS
  3. coverage: 75.0% of statements
  4. ok      tracer/dao/user 0.097s
复制代码
如果报错,这个问题是数据库中不存在表:
  1. Error 1146 (42S02): Table 'tracer.user_info' doesn't exist
复制代码
(2)结构体方法打桩方法

gomonkey.ApplyMethod()、gomonkey.ApplyMethodFunc()
区别:

  • 匹配方式不同:ApplyMethod 是名称匹配,ApplyMethodFunc 是函数本体匹配
  • 传参核心不同:ApplyMethod 必须传 reflect.Type+方法名字符串;ApplyMethodFunc 直接传 原方法函数,不需要反射
  • 底层逻辑不同:ApplyMethod 是「反射查找方法」,ApplyMethodFunc 是「直接绑定方法函数」,后者性能更高
gomonkey.ApplyMethod() 参数示例:
  1. // 第一个参数:要打桩的方法所属的类型,通过 reflect.TypeOf(实例) 获取,区分值接收者 / 指针接收者
  2. // 第二个参数:要打桩的方法名,字符串格式、大小写敏感,必须和原方法名完全一致
  3. // 第三个参数:打桩方法,入参和出参要保持和被打桩方法保持一致
  4. func ApplyMethod(target interface{}, methodName string, double interface{}) *Patches {
  5.         return create().ApplyMethod(target, methodName, double)
  6. }
复制代码
gomonkey.ApplyMethodFunc() 参数示例:
  1. // 第一个参数:要打桩的方法所属的类型,通过 reflect.TypeOf(实例) 获取,区分值接收者 / 指针接收者
  2. // 第二个参数:要打桩的方法名,字符串格式、大小写敏感,必须和原方法名完全一致
  3. // 第三个参数:打桩方法,入参和出参要保持和被打桩方法保持一致
  4. func ApplyMethodFunc(target interface{}, methodName string, doubleFunc interface{}) *Patches {
  5.         return create().ApplyMethodFunc(target, methodName, doubleFunc)
  6. }
复制代码
注意:可以不用写reflect.TypeOf(实例),如果了解方法所属类型,可以直接写方法类型,而不用reflect.TypeOf()在获取一次
gomonkey.ApplyMethod()

method_demo.go
  1. type MethodDemo struct {
  2. }
  3. func (m MethodDemo) MethodDemo(ret string) {
  4.         fmt.Println("MethodDemo:", ret)
  5. }
复制代码
method_demo_test.go
  1. // ========== 基于 gomonkey.ApplyMethod() 的单元测试 ==========
  2. func TestMethodDemo(t *testing.T) {
  3.         // 1. 初始化结构体实例(当前结构体无成员变量,直接实例化即可)
  4.         md := MethodDemo{}
  5.         // 2. 核心:使用 gomonkey.ApplyMethod() 对【值接收者方法】打桩
  6.         // 第一个参数:reflect.TypeOf(实例) → 因为是值接收者,直接传值类型实例即可
  7.         // 第二个参数:被打桩的方法名字符串(严格和原方法名一致,大小写敏感)
  8.         // 第三个参数:mock桩函数 → 入参/返回值 必须和原方法完全一致
  9.         patch := gomonkey.ApplyMethod(
  10.                 reflect.TypeOf(md),
  11.                 "MethodDemo",
  12.                 func(m MethodDemo, ret string) {
  13.                         // 自定义的mock逻辑,替代原方法的 fmt.Println 逻辑
  14.                         t.Log("mock执行成功,入参ret:", ret)
  15.                 },
  16.         )
  17.         // 铁律:延迟撤销打桩,防止污染其他测试用例,必须写!
  18.         defer patch.Reset()
  19.         // 3. 调用原方法,验证打桩是否生效
  20.         md.MethodDemo("hello gomonkey")
  21. }
复制代码
命令行执行命令
go test -run "^TestMethodDemo$" -cover
结果:
  1. PS D:\wyl\workspace\go\tracer\logic\method_demo> go test -run "^TestMethodDemo$" -cover      
  2. MethodDemo: hello gomonkey
  3. PASS
  4. coverage: 50.0% of statements
  5. ok      tracer/logic/method_demo        0.303s
复制代码
gomonkey.ApplyMethodFunc()

method_func_demo.go
  1. type MethodFuncDemo struct {
  2. }
  3. func (m MethodFuncDemo) MethodFuncDemo(ret string) {
  4.         fmt.Println("MethodFuncDemo:", ret)
  5. }
复制代码
method_func_demo_test.go
  1. // ==========  【结构体值类型绑定】对应的单元测试(纯值类型,无任何指针语法) ==========
  2. func TestMethodFuncDemo(t *testing.T) {
  3.         // 1. 初始化【结构体值类型实例】 核心✅ 无指针&,纯结构体类型绑定
  4.         mfd := MethodFuncDemo{}
  5.         // 2. 核心:gomonkey.ApplyMethodFunc 三参数打桩【结构体值类型绑定】
  6.         // 三参数固定规则:值类型 = 值实例 + 方法名字符串 + 值类型桩函数
  7.         patch := gomonkey.ApplyMethodFunc(
  8.                 mfd,              // 参数1:结构体值类型实例(核心,纯值绑定)
  9.                 "MethodFuncDemo", // 参数2:方法名字符串(和值接收者方法名一致)
  10.                 func(m MethodFuncDemo, ret string) { // 参数3:桩函数【无*号,纯结构体值类型入参】✅必匹配
  11.                         // 桩函数第一个入参必须是:纯结构体类型 MethodFuncDemo,无任何指针
  12.                         t.Log("✅ 结构体值类型绑定打桩生效,入参ret = ", ret)
  13.                 },
  14.         )
  15.         // 铁律:延迟撤销打桩,防止污染其他测试用例,必须写
  16.         defer patch.Reset()
  17.         // 3. 调用【值接收者方法】,验证结构体值类型绑定打桩结果
  18.         mfd.MethodFuncDemo("hello gomonkey 结构体值类型绑定")
  19. }
复制代码
命令行执行命令
go test -run "^TestMethodFuncDemo$" -cover
结果:
  1. PS D:\wyl\workspace\go\tracer\logic\method_demo> go test -run "^TestMethodFuncDemo$" -cover
  2. MethodFuncDemo: hello gomonkey 结构体值类型绑定
  3. PASS
  4. coverage: 25.0% of statements
  5. ok      tracer/logic/method_demo        0.284s
复制代码
(3)指针接收者方法打桩方法

gomonkey.ApplyMethod()、gomonkey.ApplyMethodFunc()
gomonkey.ApplyMethod()

method_demo.go
  1. type MethodDemo struct {
  2. }
  3. func (m MethodDemo) MethodDemo(ret string) {
  4.         fmt.Println("MethodDemo:", ret)
  5. }func (m *MethodDemo) MethodPointerDemo(ret string) {        fmt.Println("MethodPointerDemo:", ret)}
复制代码
method_demo_test.go
  1. // ========== 基于 gomonkey.ApplyMethod() 的单元测试【指针接收者专用写法】 ==========
  2. func TestMethodPointerDemo(t *testing.T) {
  3.         // 1. 初始化【指针类型】的结构体实例 (必须是指针,和方法接收者对应)
  4.         md := &MethodDemo{}
  5.         // 2. 核心:gomonkey.ApplyMethod() 打桩【指针接收者方法】
  6.         // 第一个参数:reflect.TypeOf(md) 传入指针实例,获取 *MethodDemo 的反射类型 【必须传指针实例】
  7.         // 第二个参数:方法名字符串,严格大小写一致
  8.         // 第三个参数:mock桩函数,入参规则严格匹配
  9.         patch := gomonkey.ApplyMethod(
  10.                 // 可以直接写 &MethodDemo{}
  11.                 reflect.TypeOf(md),
  12.                 "MethodPointerDemo",
  13.                 // mock桩函数规则:
  14.                 // ① 第一个入参:必须是【指针接收者】 *MethodDemo ,和原方法一致
  15.                 // ② 第二个入参:原方法的入参 ret string,和原方法一致
  16.                 // ③ 原方法无返回值,桩函数也必须无返回值
  17.                 func(m *MethodDemo, ret string) {
  18.                         // 自定义mock逻辑,替代原方法的 fmt.Println 逻辑
  19.                         t.Log("✅ 指针方法打桩生效,入参ret = ", ret)
  20.                 },
  21.         )
  22.         // 铁律:延迟撤销打桩,防止污染其他测试用例,必写!
  23.         defer patch.Reset()
  24.         // 3. 调用被打桩的指针方法,验证mock是否生效
  25.         md.MethodPointerDemo("hello gomonkey 指针接收者")
  26. }
复制代码
命令行执行命令
go test -run "^TestMethodPointerDemo$" -cover
结果:
  1. PS D:\wyl\workspace\go\tracer\logic\method_demo> go test -run "^TestMethodPointerDemo$" -cover
  2. MethodPointerDemo: hello gomonkey 指针接收者
  3. PASS
  4. coverage: 25.0% of statements
  5. ok      tracer/logic/method_demo        0.277s
复制代码
gomonkey.ApplyMethodFunc()

method_func_demo.go
  1. type MethodFuncDemo struct {
  2. }
  3. func (m MethodFuncDemo) MethodFuncDemo(ret string) {
  4.         fmt.Println("MethodFuncDemo:", ret)
  5. }func (m *MethodFuncDemo) MethodFuncPrinterDemo(ret string) {        fmt.Println("MethodFuncPrinterDemo:", ret)}
复制代码
method_func_demo_test.go
  1. // ========== 指针接收者 对应的 ApplyMethodFunc 单元测试 ==========
  2. func TestMethodFuncPointerDemo(t *testing.T) {
  3.         // 1. 初始化指针类型的结构体实例
  4.         mfd := &MethodFuncDemo{}
  5.         // 2. 核心:gomonkey.ApplyMethodFunc 打桩【指针接收者方法】
  6.         // ✅ 第一个参数:直接传【指针接收者的方法本体】 语法固定:(*结构体名).方法名
  7.         patch := gomonkey.ApplyMethodFunc(
  8.                 // 重点:指针接收者的方法本体写法
  9.                 mfd,
  10.                 "MethodFuncPrinterDemo",
  11.                 func(m *MethodFuncDemo, ret string) {
  12.                         // 桩函数第一个入参必须是 指针类型 *MethodFuncDemo
  13.                         t.Log("✅ MethodFuncPrinterDemo 指针接收者打桩生效,入参ret = ", ret)
  14.                 },
  15.         )
  16.         defer patch.Reset()
  17.         // 3. 调用指针方法
  18.         mfd.MethodFuncPrinterDemo("hello gomonkey 指针接收者")
  19. }
复制代码
命令行执行命令
go test -run "^TestMethodFuncPointerDemo$" -cover
结果:
  1. PS D:\wyl\workspace\go\tracer\logic\method_demo> go test -run "^TestMethodFuncPointerDemo$" -cover
  2. MethodFuncPrinterDemo: hello gomonkey 指针接收者
  3. PASS
  4. coverage: 25.0% of statements
  5. ok      tracer/logic/method_demo        0.275s
复制代码
(4)顺序返回不同结果

(高频实用)方法
gomonkey.ApplyMethodSeq()

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册