golang单元测试及mock总结

文章目录

    • 一、前言
      • 1、单测的定位
      • 2、vscode中生成单测
    • 二、构造测试case的注意事项
      • 1、项目初始化
      • 2、构造空interface{}
      • 3、构造结构体的time.Time类型
      • 4、构造json格式的test case
    • 三、运行单测文件
      • 1、整体运行单测文件
      • 2、运行单个单测文件报错
        • (1)command-line-arguments是什么
        • (2)undefined发生原因
        • (3)缺少初始化导致的发生panic
      • 3、查看单测覆盖率
      • 4、单测覆盖文件解读
      • 5、生成可被浏览器打开的单测文件
      • 6、单测覆盖率的问题
    • 四、关于单测粒度的问题
      • 1、chatgpt的回答
      • 2、个人理解
    • 五、mock数据
      • 1、mock组件选择
      • 2、mock实操
        • (1)mock函数调用
        • (2)mock方法调用
        • (3)mock其他包的函数
        • (4)mock循环中的函数
        • (5)mock http调用
      • 3、对于mock的看法

一、前言

1、单测的定位

      单测在软件工程中的地位毋庸置疑,它要求工程师必须去主动思考代码的边界,异常处理等等。另一方面,它又是代码最好的说明书,你的函数具体做了什么,输入和输出一目了然。

      计算机科学家Edsger Dijkstra曾说过:“测试能证明缺陷存在,而无法证明没有缺陷。”再多的测试也不能证明一个程序没有BUG。在最好的情况下,测试可以增强我们的信心:代码在很多重要场景下是可以正常工作的。

参考:go语言圣经之测试函数

2、vscode中生成单测

参考:在 VS Code 快速生成单元测试

      vscode生成单元测试如下,我们需要编写测试用例数组,明确指出来want结果以及wantErr,通过遍历的方式去执行测试用例数组。

func TestGenerateStsTokenService(t *testing.T) {
	type args struct {
		ctx             context.Context
		generateStsData *dto.GenerateStsReqParams
	}
	tests := []struct {
		name     string
		args     args
		wantResp *common.RESTResp
		wantErr  bool
	}{
		{
			name: "测试正常生成sts",
			args: args{
				ctx: context.TODO(),
				generateStsData: &dto.GenerateStsReqParams{
					SessionName: "webApp",
					AuthParams:  &dto.AuthParamsData{},
				},
			},
			wantResp: &common.RESTResp{
				Code: 0,
				Data: &dto.OssStsRespData{
				},
			},
			wantErr: false,
		},
		{
			name: "测试异常生成sts",
			args: args{
				ctx: context.TODO(),
				generateStsData: &dto.GenerateStsReqParams{
					SessionName: "liteApp",
					AuthParams:  &dto.AuthParamsData{},
				},
			},
			wantResp: &common.RESTResp{
				Code: 20003,
				Data: interface{}(nil),
			},
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
 
			gotResp, err := GenerateStsTokenService(tt.args.ctx, tt.args.generateStsData)
			if (err != nil) != tt.wantErr {
				t.Errorf("GenerateStsTokenService() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if !reflect.DeepEqual(gotResp, tt.wantResp) {
				t.Errorf("GenerateStsTokenService() = %v, want %v", gotResp, tt.wantResp)
			}
		})
	}
}

二、构造测试case的注意事项

1、项目初始化

// TestMain会在执行其他测试用例的时候,自动执行
func TestMain(m *testing.M) {
    setup()  //初始化函数
    retCode := m.Run() // 运行单元测试
    teardown() //后置校验,钩子函数,可不实现
    os.Exit(retCode) //清理结果
}

2、构造空interface{}

// 直接给Data赋值为nil的话,验证会失败,
// 单纯的nil和(*infra.QueryOneMappingCode)(nil)是不一样的
wantResp: &common.RESTResp{
				Code:    0,
				Message: "",
				Data:    (*infra.QueryOneMappingCode)(nil),
			},

// 数组类型的空
// []dto.OneMappingCode{}也会验证失败
wantRes: []dto.OneMappingCode(nil),

3、构造结构体的time.Time类型

Data: &infra.xxx{
					ID:          54,
					Code:        "338798",
					TakerUid:    "",
					State:       1,
					Type:        1,
					CreatedAt: time.Date(2023, time.June, 9, 16, 32, 59, 0, time.Local),
				},

也可以直接打印接口的返回,看看CreatedAt返回的是什么,然后构造一下就可以。
t.Logf("gotResp:(%#v)", gotResp.Data)

4、构造json格式的test case

wantResp: &common.RESTResp{
				Code:    0,
				Message: "success",
				Data: `{
					"id": 54,
					"code": "338798",
					"creator_uid": "12345",
					"client_appId": "1234",
					"taker_uid": "",
					"state": 1,
					"type": 1,
					"created_at": "2023-06-09T16:32:59+08:00"
				   }`,
			},

三、运行单测文件

1、整体运行单测文件

  cd /xxx 单测目录
  go test
  成功输出:
  PASS
  ok

2、运行单个单测文件报错

错误提示如下:

# command-line-arguments [command-line-arguments.test]
./base_test.go:26:18: undefined: Ping

      明明Ping函数和单测文件都在同一个包下面,为什么会出现undefined呢?command-line-arguments是什么?
答:

(1)command-line-arguments是什么

go test [flags] [packages] [build flags] [packages]
命令行参数中指定的每个包或文件都将被视为一个要进行测试的包。而 "command-line-arguments" 
这个标识符就是用来表示上述情况中命令行参数中指定的文件。

这样可以使 go test 命令将指定的文件作为单独的包进行处理,并执行其中的测试函数。

(2)undefined发生原因

错误提示build失败,也就是说我们需要把单测文件依赖的文件也传入进去。比如我这里单测base_test.go文件,则需要把base.go也写到命令行参数中。
具体参考:【Golang】解决Go test执行单个测试文件提示未定义问题

go test ./base.go ./base_test.go

(3)缺少初始化导致的发生panic

一般来说我们在一个package下,定义一个TestMain()函数就可以了,进行代码的初始化。但是当我们需要运行单个测试文件的时候,有可能这个测试文件里面恰好没有TestMain()了咋整。

api_test.go
	TestMain()
base_test.go // 没有TestMain()函数

// 解决方案
1、初始化代码放到setup()函数中
2go命令行
go test ./base.go ./base_test.go ./api_test.go ./api.go
3、只想运行base_test.go怎么办
	base_test.go中加上自己的setuoBase()

3、查看单测覆盖率

go test -cover
	coverage: 80.4% of statements

4、单测覆盖文件解读

go test -coverprofile=coverage.out

// 打开单测覆盖率文件
mode: set
base.go:10.118,14.23 3 1
base.go:14.23,17.3 2 1

	解释如下:
	10.118,14.23 3 1 表示第 10 行到第 14 行代码被测试覆盖到了,且覆盖
	率为 3/1 (300%)。这是因为第 10 行至少执行了一次,如果执行了三次,则覆盖率为 300%14.23,17.3 2 1 表示第 14 行到第 17 行代码被测试覆盖到了,且覆盖率为 2/1 (200%)

5、生成可被浏览器打开的单测文件

go test -coverprofile=coverage.out
go tool cover -html=coverage.out -o coverage.html

绿色代表被覆盖到的代码,红色代表没有被覆盖到的代码。
左上角是运行单测命令目录下,所有go文件的覆盖率。
可以考虑新增单测case来覆盖到这部分红色。
在这里插入图片描述

6、单测覆盖率的问题

      覆盖率为 100% 表示测试用例覆盖了所有的可能执行路径,即程序的所有功能都被覆盖到了。而覆盖率高于 100% 则表示相同的代码路径被多次测试或某些代码行在被测试期间被执行了多次。

      但是单测100%并不能保证没有bug,只能保证写出来的代码没问题,但逻辑或者业务上的漏洞是检测不到的。

      博主在滴滴的组是建议单测覆盖率50%以上,其他朋友的公司要求核心接口必须有单测,整体单测覆盖率30%以上。有需要的可以参考下。

四、关于单测粒度的问题

      写单测的时候,总会疑问到底要写的多细呢?特别是原来项目没有单测的时候,补单测的代码比业务逻辑代码还多。。。
本例中,目录结构如下:

domain:
	base.go
	code.go
	code_test.go
	util.go

code.go会调用base.goutil.go的函数,运行code_test.go发现单测覆盖率
已经80%了,是不是意味着只需要写个code_test.go就可以了呢?

1、chatgpt的回答

      实际上不是的,base.goutil.go后续还可能被其他的文件使用,我们写单测的时候,应该尽量覆盖所有的异常情况,也就是程序的边界问题。因此base.goutil.go也需要做对应的单测,这样才能得到高质量的代码。

2、个人理解

      单个code_test.go文件导致的问题是下层函数不mock,可能会影响到实际的数据,导致单测只能运行一次,而不能一直PASS。其次是代码流程变长导致单测case越写越多,接近集成测试了,这不是我们单测的目标。

      把code_test.go中关于base.goutil.go的函数都给mock掉,发现单测覆盖率只有37%,且测试路径比较短。还需要分别写base_test.go和util_test.go,写完util_test.go单测覆盖率立马82%

      拆分的粒度变细,更加关注每个函数的输入和输出。特别是当修改某个函数的时候,只需要使用对应的单测来进行验证,而不需要从入口处进行测试。毕竟单元测试不是集成测试。

参考:
Golang 单元测试:有哪些误区和实践?
Go的单元测试技巧

五、mock数据

      在写单测的时候,程序难免会出现各种跨文件的函数调用,以及操作第三方中间件或者上下游交互的情况,这个时候mock就显得尤为重要。

      想象下,没有mock的时候,我们运行单测可能就会写入一次数据库?或者对下游发起一次请求?这样的单测,怕是只能运行一次哟。mock的出现让我们关注代码的实现细节,不会担心会造成数据污染或者单测只能运行一遍就GG的情况。

1、mock组件选择

参考:如何做好单元测试?Golang Mock”三剑客“ gomock、monkey、sqlmock
GO进阶单元测试

在这里插入图片描述

      博主这里更喜欢无侵入的mock,直接一把梭。可惜monkey已经不更新了,现在都是用gomonkey,国人大佬开发的

gomonkey 项目库
解析 Golang 测试(8)- gomonkey 实战

2、mock实操

(1)mock函数调用

      函数中存在大量的封装调用,比如A->BA->C这种,因此自由mock BC函数对我们的单元测试来说还是很重要的。

patches := gomonkey.ApplyFunc(queryOneMappCode, func(ctx context.Context, code string) (*infra.QueryOneMappingCode, error) {
				// 参数大于6则返回空
				if len(code) > 6 {
					return nil, nil
				}
				return &infra.QueryOneMappingCode{
					ID:          54,
					Code:        "338798",
					CreatedAt:   time.Date(2023, time.June, 9, 16, 32, 59, 0, time.Local),
				}, nil
			})
			defer patches.Reset()

(2)mock方法调用

1、实例化接口
var mockProvider = provider.Test
// 接口如下
type TestDbProvider interface {
	SetDb(db *sqlx.DB)
	GetOne(dest interface{}, sql string, args interface{}) (resp *infra.QueryOneMappingCode, err error)
}


2、mock对应的查询方法
// 注意,第一个参数不能是指针,不然mock会失效
// 例如 var oss_bucket_obj *oss.Bucket ,传入target为: *oss_bucket_obj
// 传地址会报错
patches := gomonkey.ApplyMethodFunc(mockProvider, "GetOne", func(dest interface{}, sql string, args interface{}) (resp *infra.QueryOneMappingCode, err error) {
				code := args.(string)
				if code == "123456" {
					return &infra.QueryOneMappingCode{
						ID:          1,
						Code:        "123456",
						CreatedAt:   time.Date(2023, time.June, 9, 16, 32, 59, 0, time.Local),
					}, nil
				} else if code == "456789" {
					return &infra.QueryOneMappingCode{
						ID:          1,
						Code:        "456789",
						CreatedAt:   time.Date(2023, time.June, 9, 16, 32, 59, 0, time.Local),
					}, nil
				} else {
					return nil, nil
				}
			})
			defer patches.Reset()

(3)mock其他包的函数

xx_test文件中直接引用其他包即可。一般xx_test.goxx.go在同一个包下,所以也不用担心出现循环引用的问题。

patches := gomonkey.ApplyFunc(util.GenerateRandomCode, func(numDigits int) string {
				return "123456"
			})
			defer patches.Reset()

(4)mock循环中的函数

比如在A函数中,循环3次调用了B函数,那么mock如下:

createA := &infra.CreateMappingCode{Code: "933903"}
			createB := &infra.CreateMappingCode{Code: "601690"}
			createC := &infra.CreateMappingCode{Code: "798493"}
			p := gomonkey.ApplyFuncSeq(structureMappingCodeRecord, []gomonkey.OutputCell{
				{Values: gomonkey.Params{createA}},
				{Values: gomonkey.Params{createB}},
				{Values: gomonkey.Params{createC}},
			})
			defer p.Reset() // 恢复原始函数

(5)mock http调用

// vscode自动生成的test代码
for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// mock httptest
			ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				if r.Method != http.MethodGet {
					w.WriteHeader(http.StatusNotFound)
				}
				// 构造返回参数
				w.WriteHeader(http.StatusOK)
				// 获取POST请求的参数,根据参数返回不同的响应
				bodyBytes, err := io.ReadAll(r.Body)
				if err != nil {
					// 处理错误
					w.WriteHeader(http.StatusBadRequest)
				}
				// 获取post参数
				params := new(dto.GenerateStsReqParams)
				json.Unmarshal(bodyBytes, params)
				// 根据传递的参数返回不同的响应
				res := new(common.RESTResp)
				if params.SessionName == "webApp" {
					res = &common.RESTResp{
						Code:    0,
						Message: "success",
						Data: &dto.OssStsRespData{
							Region:          "hangzhou",
							Bucket:          "test",
						},
					}
				} else {
					res = &common.RESTResp{
						Code:    1,
						Message: "failed",
						Data:    &dto.OssStsRespData{},
					}
				}
				// 模拟接口的返回,http接口返回是字节数据,因此需要json.Marshal
				jsonStr, _ := json.Marshal(res)
				w.Write(jsonStr)
			}))
			defer ts.Close()
			// 替换原来的url为mock的url
			GenerateOssStsUrl = ts.URL
    	// 发起请求,请求中的http会被mock掉
			gotResp, err := GenerateStsTokenService(tt.args.ctx, tt.args.generateStsData)
			if (err != nil) != tt.wantErr {
				t.Errorf("GenerateStsTokenService() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			t.Logf("gotResp:(%#v) ,wantResp:(%#v)", gotResp, tt.wantResp)
			if !reflect.DeepEqual(gotResp, tt.wantResp) {
				t.Errorf("GenerateStsTokenService() = %v, want %v", gotResp, tt.wantResp)
			}
		})
	}

3、对于mock的看法

对于mock,有以下两种态度

一方的人主张不要滥用mock,能不mock就不mock。被测单元也不一定是具体的一个
函数,可能是多个函数本来就应该串起来,必要的时候再mock。

一方则主张将被测函数所有调用的外面函数全部mock掉,只关注被测函数自己的
一行行代码,只要调用其他函数,全都mock掉,用假数据来测试。

本来处于懒惰和少写单测的角度,我是支持第一种方式的。

例如:
单测函数:A函数
内部逻辑:
	A->B : B函数全是业务逻辑
	A->C : C函数包括mysql或者redis操作
	A->D->E: D函数纯业务逻辑,构造请求参数。E函数对外发起http请求

      第一种方式是只mock CE函数,测试A函数的时候,会把BD也测试到。主打一个省事快捷。

      直到我遇到了更复杂的场景,B里面还有B1B2函数,D里面有D1D2函数,逻辑非常复杂的情况下,第一种方式就变成了集成测试。单测用例慢慢变成了测试用例。 比如只修改D2函数的情况下,要修改和通过单测A进行测试。。。。

      第二种方式,就是在每一层都mock掉外部调用。单测A就只关注A的逻辑,mockB,C,D,E,只关注B,C,D,E输出是正确或者错误的情况。
针对B,C,D,E函数又有自己的单测函数,充分覆盖掉。这样当修改D2函数的时候,只需要修改和通过D2的单测即可。

      对于外部依赖,比如第三方库mysql,redis,mq这种统一进行mock。 对于内部的函数调用,建议是粒度细一些,A_test.go就只对A.go里面的逻辑负责。至于调用B.go的部分,就交给B_test.go吧。

end

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/38043.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

无法找到docker.sock

os环境:麒麟v10(申威) 问题描述: systemctl start docker 然后无法使用docker [rootnode2 ~]# systemctl restart docker [rootnode2 ~]# docker ps Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon r…

PLEX如何搭建个人局域网的视频网站

Plex是一款功能非常强大的影音媒体管理系统,最大的优势是多平台支持和界面优美,几乎可以在所有的平台上安装plex服务器和客户端,让你可以随时随地享受存储在家中的电影、照片、音乐,并且可以实现观看记录无缝衔接,手机…

PROFINET转TCP/IP网关TCP/IP协议的含义是

大家好,今天要和大家分享一款自主研发的通讯网关,远创智控YC-PN-TCPIP。这款网关可是集多种功能于一身,PROFINET从站功能,让它在通讯领域独领风骚。想知道这款网关如何实现PROFINET和TCP/IP网络的连接吗?一起来看看吧&…

iPad远控Windows解决方案

最近入手了一台iPad,但我不想让它沦为爱奇艺的工具,遂考虑如何在iPad上获得桌面级Windows的生产力。主要还是之前背着电脑出远门太累了,这也是促成我买iPad的重要因素。 一种方案就是通过远程控制,在iPad上远程操作自己的电脑&am…

C# PaddleInference OCR 表格识别

效果 项目 VS2022.net4.8OpenCvSharp4Sdcb.PaddleInferenceSdcb.PaddleOCR 测试图片 代码 using OpenCvSharp.Extensions; using OpenCvSharp; using Sdcb.PaddleInference; using Sdcb.PaddleOCR; using Sdcb.PaddleOCR.Models; using Sdcb.PaddleOCR.Models.Details; using…

一次零基础靶机渗透细节全程记录

一、打靶总流程 1.确定目标: 在本靶场中,确定目标就是使用nmap进行ip扫描,确定ip即为目标,只是针对此靶场而言。其他实战中确定目标的方式包括nmap进行扫描,但不局限于这个nmap。 2.信息收集: 比如平常挖…

数据结构(2.1)——时间复杂度和空间复杂度计算

前言 (1)因为上一篇博客:数据结构(2)—算法对于时间复杂度和空间复杂度计算的讲解太少。所以我在次增加多个案例讲解。 (2)上一篇已经详细介绍了,为什么我们的算法要使用复杂度这一个…

Stable Diffusion (持续更新)

引言 本文的目的为记录stable diffusion的风格迁移,采用diffusers example中的text_to_image和textual_inversion目录 2023.7.11 收集了6张水墨画风格的图片,采用textual_inversion进行训练,以"The street of Paris, in the style of …

uniApp之同步资源失败,未得到同步资源的授权,请停止运行后重新运行,并注意手机上的授权提示、adb、shell、package、uninstall

文章目录 背景解决思路执行查找第三方应用的指令执行卸载指令 背景 一开始正常编译运行,由于应用页面有些许奇怪的错误,便想着卸载,重新运行安装调试基座。卸载后,运行还是会出现,明明已经把应用卸载了,还是…

基于深度学习的高精度Caltech行人检测系统(PyTorch+Pyside6+YOLOv5模型)

摘要:基于深度学习的高精度Caltech数据集行人检测识别系统可用于日常生活中或野外来检测与定位行人目标,利用深度学习算法可实现图片、视频、摄像头等方式的行人目标检测识别,另外支持结果可视化与图片或视频检测结果的导出。本系统采用YOLOv…

HTTP、HTTPS协议详解

文章目录 HTTP是什么报文结构请求头部响应头部 工作原理用户点击一个URL链接后,浏览器和web服务器会执行什么http的版本持久连接和非持久连接无状态与有状态Cookie和Sessionhttp方法:get和post的区别 状态码 HTTPS是什么ssl如何搞到证书nginx中的部署 加…

什么是人工智能大模型?

目录 1. 人工智能大模型的概述:2. 典型的人工智能大模型:3. 人工智能大模型的应用领域:4. 人工智能大模型的挑战与未来:5. 人工智能大模型的开发和应用:6. 人工智能大模型的学习资源: 人工智能大模型是指具…

计数排序

计数排序 排序步骤 1、以最大值和最小值的差值加一为长度创建一个新数组 2、将索引为0对应最小值,索引为1对应最小值1,索引为2对应最小值2,以此类推,将索引对应最小值到最大值之间所有的值 3、遍历一遍,遇到一个数字…

MyBatis学习笔记之首次开发及文件配置

文章目录 MyBatis概述框架特点 有关resources目录开发步骤从XML中构建SqlSessionFactoryMyBatis中有两个主要的配置文件编写MyBatis程序关于第一个程序的小细节MyBatis的事务管理机制JDBCMANAGED 编写一个较为完整的mybatisjunit测试mybatis集成日志组件 MyBatis概述 框架 在…

Excel VLOOKUP使用详解

VLOOKUP语法格式: VLOOKUP(lookup_value,table_array,col_index_num,range_lookup) VLOOKUP(要查找的值,查找区域,要返回的结果在查找区域的第几列,精确匹配或近似匹配) 一、精确查找 根据姓名查找对应…

FPGA Verilog移位寄存器应用:边沿检测、信号同步、毛刺滤波

文章目录 1. 端口定义2. 边沿检测3. 信号同步4. 信号滤波5. 源码6. 总结 输入信号的边沿检测、打拍同步、毛刺滤波处理,是FPGA开发的基础知识,本文介绍基于移位寄存器的方式,实现以上全部功能:上升沿、下降沿、双边沿检测、输入信…

个人使用:Windows下 OpenCV 的下载安装(2021.12.4详细)

一、下载OpenCV   到OpenCV官网Release(发布)板块下载OpenCV-4.5.4 Windows。 下载后是这样的 然后双击他,解压,就是大佬们说的安装,实质就是解压一下,解压完出来一个文件夹,其他什么也没发生。你把这个文件夹放在哪…

STM32(HAL库)驱动SHT30温湿度传感器通过串口进行打印

目录 1、简介 2、CubeMX初始化配置 2.1 基础配置 2.1.1 SYS配置 2.1.2 RCC配置 2.2 软件IIC引脚配置 2.3 串口外设配置 2.4 项目生成 3、KEIL端程序整合 3.1 串口重映射 3.2 SHT30驱动添加 3.3 主函数代 3.4 效果展示 1、简介 本文通过STM32F103C8T6单片机通过HAL库…

uniapp uni实人认证

uni实人认证依赖 目前仅支持App平台。 h5端活体人脸检测,使用的是百度云的h5人脸实名认证 使用要求 1、app端 在使用前,请确保您已注册DCloud账号,并已完成实名认证。 然后需要按文档开通服务 业务开通 | uni-app官网 2、h5端 在使用前…

STM32 ws2812b 最快点灯cubemx

文章目录 前言一、cubemx配置二、代码1.ws2812b.c/ws2812b.h2.主函数 前言 吐槽 想用stm32控制一下ws2812b的灯珠,结果发下没有一个好用的。 emmm!!! 自己来吧!!!! 本篇基本不讲原理…