golang单元测试及mock总结

news2024/11/14 22:04:39

文章目录

    • 一、前言
      • 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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

fileclude

背景知识 文件包含漏洞 题目 分析上述代码 file2被放入file_get_contents()函数,且要求返回值为hello ctf file1是要包含的文件,放在include函数中 用php://filter伪协议读取源代码 构造payload: file1php://filter/readconvert.base64-…

Loki+Promtail+Grafana 监控 K8s 日志

Loki 架构: 1、loki:服务端,负责存储日志和处理查询 2、promtail:采集端,负责采集日志发送给loki 3、grafana:负责采集日志的展示 创建 yaml 文件 cat loki-rbac.yaml apiVersion: v1 kind: ServiceAccount…

HarmonyOS应用开发-第一章-DevEco Studio的安装

一、前言 本栏可以帮助正在学习HarmonyOS应用开发的开发者快速上手和掌握。 二、安装步骤 首先下载DevEco Studio(HarmonyOS应用的集成开发境),点击:IDE下载页面,点击立即下载。 下载完成后,双击运行安装程…

消息队列——RabbitMQ基本概念+容器化部署和简单工作模式程序

目录 基本概念 MQ 的优势 1.应用解耦 2.异步提速 3.削峰填谷 MQ 的劣势 使用mq的条件 常见MQ产品 RabbitMQ简介 RabbitMQ的六种工作模式 JMS RabbitMQ安装和配置。 RabbitMQ控制台使用。 RabbitMQ快速入门——生产者 需求: RabbitMQ快速入门——消费者 小结 基本概…

golang slice参数传递

在介绍slice函数参数传递之前,先介绍一下slice的结构 type slice struct {array unsafe.Pointerlen intcap int }这个应该周知了,也不必多解释,需要注意两个问题 1、如何初始化slice 我们知道初始化slice有几种方式,注意以…

Spring Batch之读数据—读多文件(三十三)

一、读多文件 前面的所有文件的读取基本上是对单文件执行的,在实际应用中,我们经常操作批量的文件。 Spring Batch框架提供了现有的组件MultiResourceItemReader支持对多文件的读取,通过MultiResourceItemReader读取批量文件非常简单。MultiR…

【算法与数据结构】144、145、94LeetCode二叉树的前中后遍历

文章目录 一、题目二、递归算法三、完整代码 所有的LeetCode题解索引,可以看这篇文章——【算法和数据结构】LeetCode题解。 一、题目 二、递归算法 思路分析:这道题比较简单,不多说了,大家直接看代码就行。注意前中后遍历是指中间…

01Matlab编程基础

回忆你所学过的数学函数并给出x3.56 时以下函数的值 s i g n ( x ) , x a ( a 3 ) , sin ⁡ ( x ) , cos ⁡ ( x ) , tan ⁡ ( x 2 ) , 2 tan ⁡ ( x ) \begin{aligned}sign\left( x\right) ,x^{a}\left( a3\right) ,\sin \left( x\right) ,\cos \left( x\right) ,\tan \left(…

MiniGPT4系列之二推理篇命令行方式:在RTX-3090 Ubuntu服务器推理详解

MiniGPT4系列之一部署篇:在RTX-3090 Ubuntu服务器部署步骤详解_seaside2003的博客-CSDN博客 MiniGPT4系列之二推理篇命令行方式:在RTX-3090 Ubuntu服务器推理详解_seaside2003的博客-CSDN博客 MiniGPT4系列之三模型推理 (Web UI):在RTX-309…

如何更简洁查看接口返回的树状图信息

首先,你的接口返回的得是树状图信息。在浏览器上访问接口: 按下f12 刷新页面 点击就可以看到层级关系了。当然也可以使用下面这个插件对数据进行格式化。

行列式计算

举例: 1.暴力计算 2.通过代数余子式计算 相关理论: 这个C就是上图的Aij哈,我拷的别人的图。 可以得出,行列式的值可以按照某行展开,展开后余子式即为一个新的行列式,就是原行列式删除某一行一列之后得到的…

Java 设计模式——适配器模式

目录 1.概述2.结构3.类适配器模式3.1.目标接口3.2.被适配类3.3.适配器类3.4.测试 4.对象适配器模式5.优缺点6.应用场景7.JDK 源码解析——InputStreamReader 1.概述 (1)如果去欧洲国家去旅游的话,他们的插座如下图最左边,是欧洲标…

vue3- 02vue3的变化

1. main.js 创建实例不再使用构造函数&#xff0c;而是使用createApp使用插件时不再通过构造函数&#xff0c;而是通过实例 2. 组件 1. this指向不同 vue2的this指向是组件vue3的this指向是proxy&#xff08;代理&#xff0c;代理的是组件实例&#xff09; <template&…

分布式软件架构——传输链路

传输链路 链路指无源的点到点的物理连接。链路是计算机网络中的一个重要概念&#xff0c;它指的是连接两个网络设备的物理或逻辑路径。简单来说&#xff0c;链路就是电信号或数据在网络中传输的路径。在计算机网络中&#xff0c;链路可以分为物理链路和逻辑链路两种。物理链路…

传承与进取的力量-节选

只简单谈如下两点&#xff1a; 传承&#xff1a;家族各类关系网总和 进取&#xff1a;个人提升获取资源和 少数人的晚餐 之前&#xff0c;每一届都会在交流中谈及&#xff0c;时间才是真正的公平公正&#xff0c;生命只有一次&#xff0c;至少在目前还没有公开报道的永生人。…

动态内存分配(2)——经典例题的讲解

前言&#xff1a; 在前面我们已经学习动态分配内存&#xff0c;今天我们就来做一做它的几道经典例题&#xff0c;加深巩固我们所学的知识。 知识复习&#xff1a;动态内存管理&#xff08;1&#xff09;_从前慢&#xff0c;现在也慢的博客-CSDN博客 题目1&#xff1a; 下面代码…

福利!打造自己的ChatGPT聊天小程序,前后端代码全开源

简介 本文分享一个我前几个月实现的一个智能聊天系统小项目&#xff0c;包含了java后端&#xff0c;微信小程序端&#xff0c;web页面端三个子工程。 代码已经全部开源&#xff0c;地址放在了文末。 最近一年&#xff0c;chatGPT的火爆程度&#xff0c;已经不需要我再多说了…

使用docker简单创建一个python容器

/root/docker_python目录结构&#xff1a; . |-- demo | -- main.py -- docker-compose.ymlmain.py内容&#xff1a; # codingutf-8 # -*- coding: utf-8 -*-if __name__ __main__:print("hello world")docker-compose.yml内容&#xff1a; version: "3&q…

Spark高级特性

spark shuffle 中 map 和 reduce 是一个相对的概念&#xff0c;map是产生一批数据&#xff0c;reduce是接收一批数据&#xff0c;前一个任务是map&#xff0c;后一个任务是reduce。 hashShuffle&#xff1a;hash分组&#xff0c;一个task里面按hash值的不同&#xff0c;分到不…

7.Java 运算符

运算符分成以下几组 算术运算符关系运算符位运算符逻辑运算符赋值运算符其他运算符 1.算术运算符 public class Test {public static void main(String[] args) {int a 10;int b 20;int c 25;int d 25;System.out.println("a b " (a b) );System.out.print…