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.ContextgenerateStsData *dto.GenerateStsReqParams}tests := []struct {name     stringargs     argswantResp *common.RESTRespwantErr  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成功输出:PASSok

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.goTestMain()
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 -covercoverage: 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.gocode.gocode_test.goutil.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 httptestts := 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.MarshaljsonStr, _ := json.Marshal(res)w.Write(jsonStr)}))defer ts.Close()// 替换原来的url为mock的urlGenerateOssStsUrl = 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.mzph.cn/news/6077.shtml

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

相关文章

基于sklearn计算precision、recall等分类指标

文章目录 一、分类指标函数1.1 precision_score函数1.2 recall_score函数1.3 accuracy_score函数1.4 f1_score函数1.5 precision_recall_curve函数1.6 roc_curve函数1.7 roc_auc_score函数1.8 classification_report函数 二、二分类任务三、多分类任务3.1 Macro Average&#x…

怎样原生制作lis的CentOS容器镜像

本文介绍从一个空白的裸机CentOS自己构造检验允许的docker环境。来达到运行环境的高度定制,而不是只能依赖VS或者微软或者数据库厂商提供的镜像当做基础制作。更容易理解基础原理。最终输出产物为lisnew.tar,一个开箱即用的lis运行环境。 制作的整个过程…

os.environ[“CUDA_VISIBLE_DEVICES“]学习总结

今天发现一个很有意思的东西 import torch import os # Specify the GPU device os.environ["CUDA_VISIBLE_DEVICES"] "1" print(torch.cuda.is_available())但是如果修改下面的设置后,结果就变成了 import torch import os # Specify the…

Spring MVC -- 返回数据(静态页面+非静态页面+JSON对象+请求转发与请求重定向)

目录 1. 返回静态页面 2. 返回非静态页面 2.1 ResponseBody 返回页面内容 2.2 RestController ResponseBody Controller 2.3 示例:实现简单计算的功能 3. 返回JSON对象 3.1 实现登录功能,返回 JSON 对象 4. 请求转发(forward)或请求重定向(redirect) 4.1 请…

Rust之泛型、特性和生命期(四):验证有生存期的引用

开发环境 Windows 10Rust 1.71.0 VS Code 1.80.1 项目工程 这里继续沿用上次工程rust-demo 验证具有生存期的引用 生存期是我们已经在使用的另一种泛型。生存期不是确保一个类型具有我们想要的行为,而是确保引用在我们需要时有效。 我们在第4章“引用和借用”一…

<Java物联网> 从主动到被动:Java中的BACnet设备属性查询

目录 BACnet 使用软件 资源 模拟器 使用Java主动查 引入maven 创建网络对象 获取远程设备 获取设备属性 使用DeviceEventAdapter订阅 初始化本地BACnet设备和IP网络配置: 启动本地设备和添加监听器: 搜寻远程设备: 发送订阅COV报…

python try/except/finally

稍微总结一下&#xff0c;否则总是忘。 x abc def fetcher(obj, index): return obj[index] fetcher(x, 4) 输出&#xff1a; File "test.py", line 6, in <module> fetcher(x, 4) File "test.py", line 4, in fetcher return obj[index] …

zookeeper的应用

Zookeeper的配置文件解析: Zookeeper内部原理: 选举机制 半数机制:在集群环境中半数以上的机器存活,这个集群可用,所以在设计Zookeeper集群系统时&#xff0c;通常会选择 奇数台服务器来搭建Zookeeper的集群 虽然在配置文件中并没有指定Master和Slave。但是&#xff0c;Zookeep…

第三十二章:MySQL事务日志

第三十二章&#xff1a;MySQL事务日志 32.1&#xff1a;概述 事物有4种特性&#xff1a;原子性、一致性、隔离性和持久性。那么事务的四种特性到底是基于什么机制实现呢&#xff1f; 事物的隔离性有锁机制实现。而事物的原子性、一致性和持久性由事物的redo日志和undo日志来…

Redis入门基础命令

文章目录 一、redis1.1 redis概述1.2 redis安装 二、string2.1 基础命令2.2 存储结构2.3 应用 三、list3.1 基础命令3.2 应用 四、hash4.1 基础命令4.2 存储结构4.3 应用 五、set5.1 基础命令5.2 存储结构5.3 应用 六、zset6.1 基础命令6.2 存储结构6.3 应用 一、redis 1.1 re…

【C#】MVC页面常见的重定向方式和场景

本篇文章主要简单讲讲&#xff0c;C# MVC 页面常见跳转或者重定向的方式和场景。 在实际项目开发中&#xff0c;在一些特定场景肯定会用到重定向&#xff0c;比如&#xff1a;不同角色跳转到不同视图地址 目录 一、种常见重定向方式1.1、RedirectToAction1.2、RedirectToRoute1…

Java中的队列

队列的理解 队列&#xff08;Queue&#xff09;是一种特殊的线性表&#xff0c;它只允许在表的前端进行删除操作&#xff0c;而在表的后端进行插入操作。 LinkedList类实现了Queue接口&#xff0c;因此我们可以把LinkedList当成Queue来用。 常用方法 实例 import java.util…

Stable Diffusion服务环境搭建(远程服务版)

Stable Diffusion服务环境搭建&#xff08;远程服务版&#xff09; Stable Diffusion是什么 Stable diffusion是一个基于Latent Diffusion Models&#xff08;潜在扩散模型&#xff0c;LDMs&#xff09;的文图生成&#xff08;text-to-image&#xff09;模型。具体来说&#…

C# IO FileStream流(一)使用整理

一、C# IO 文件流&#xff0c;常用操作整理 来自其他开发者的整理&#xff1a; 文件操作常用相关类 1)Directory //操作目录&#xff08;文件夹&#xff09;&#xff0c;静态类。2)Path//静态类&#xff0c;对文件或目录的路径进行操作&#xff08;很方便&#xff09;【字符…

[深度学习实战]基于PyTorch的深度学习实战(中)[线性回归、numpy矩阵的保存、模型的保存和导入、卷积层、池化层]

目录 一、前言二、线性回归2.1 训练代码2.2 绘图部分代码2.3 numpy 数组的保存和导入代码2.4 完整代码 三、numpy矩阵的保存四、模型的保存和导入4.1 保存模型4.2 导入模型 五、卷积层5.1 Conv2d5.1.1 函数定义5.1.2 参数说明5.1.3 测试代码5.1.4 最终结果 5.2 Conv1d5.2.1 函数…

element ui 上传控件携带参数到后端

1.携带固定参数&#xff1a; 2.携带不固定参数&#xff1a; <el-row> <el-col :span"24"> <el-upload :multiple"false" :show-file-list"false" :on-success"f_h…

ShardingSphere分库分表实战之MySQL主从集群搭建

&#x1f680; ShardingSphere &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&…

Java并发编程学习笔记(一)线程的入门与创建

一、进程与线程 认识 程序由指令和数据组成&#xff0c;简单来说&#xff0c;进程可以视为程序的一个实例 大部分程序可以同时运行多个实例进程&#xff0c;例如记事本、画图、浏览器等少部分程序只能同时运行一个实例进程&#xff0c;例如QQ音乐、网易云音乐等 一个进程可以…

【C++进阶之路】适配器、反向迭代器、仿函数

文章目录 前言一、适配器①模拟实现栈②模拟实现对列 二、反向迭代器三、仿函数总结 前言 我们先来笼统的介绍一下今天的三个内容。 适配器——简单的理解就是复用&#xff0c;用已经实现的轮子&#xff0c;来继续实现某种功能。 反向迭代器——原理很简单&#xff0c;就是对…

摄影测量-共线方程、共面方程

1、共线方程 在摄影测量中&#xff0c;绝大多数的结算方法都是基于共线条件方程式的&#xff0c;如单片空间后方交会解法、像对空间前方交会解法、光束法区域网评查以及直接线性变换等。 2、共面方程 描述像片对内摄影基线以及同名光线位于同一平面的一种条件方程。在摄影测量…