通过Mock玩转Golang单元测试!

1.单元测试中的困难

如果项目中没有单元测试,对于刚刚开始或者说是规模还小的项目来说,效率可能还不错。但是一旦项目变得复杂起来,每次新增功能或对旧功能的改动都要重新手动测试一遍所有场景,费时费力,而且还有可能因为疏忽导致漏掉一些覆盖不到的点。在这个基础上,单元测试的好处就显现了出来。在单元测试覆盖比较全面的项目中进行开发,不需要耗费大量的时间去手动测试;并且在重构的时候也可以很轻松的验证代码逻辑的正确性。

而在日常的开发中,想编写一个好的单元测试也是不容易的,因为一般我们的代码不是单纯的流程控制,有着统一规范的输入输出,大多数都是依赖着外部系统,例如:数据库,网络,第三方接口等等。对于这种情况,我们很难单纯通过Golang标准库去编写好的单元测试,这时候我们就需要借助第三方的Mock工具来帮助我们完成单元测试。

2.Web服务器Mock

httptest 是Go标准库提供的Web服务器Mock工具,让我们模拟一个场景来看看httptest是怎么Mock Web服务器的:

假设现在我们要开发一个公司内部的系统,让公司员工都可以直接使用,所以需要接入公司统一的登录接口,让我们来写一个简单的用户登录权限校验方法。

func ValidateUserAuth(username, password, authUrl string) bool {
   body, _ := json.Marshal(struct {
      Username string
      Password string
   }{username,
      password})
   //调用外部接口
   request, _ := http.NewRequest(http.MethodPost, authUrl, bytes.NewReader(body))
   client := http.Client{}
   response, _ := client.Do(request)
   //如果返回的状态码为200则表示用户验证成功
   return response.StatusCode == http.StatusOK
}

其中,authUrl参数为公司统一的权限校验接口,判断用户是否为公司员工。当然,这种参数传递方式可能不会出现在真正的代码中,没关系,我们先看这种方式,后面会优化为其他方式。

如果我们要对这个方法写一个单元测试的话,我们肯定是不能传真正的authUrl来测试的,因为这是外部系统提供的接口,我们无法保证它的稳定性。因此用于模拟web服务器行为的httptest就派上用场了。

func TestValidateUserAuth(t *testing.T) {
   //函数类型的变量,用户定义Web服务器行为
   var handler http.HandlerFunc = func(writer http.ResponseWriter, request *http.Request) {
        writer.WriteHeader(200)
        writer.Write(nil)
   }
   mockAuthServer := httptest.NewServer(handler)
   success := ValidateUserAuth("user", "passwd", mockAuthServer.URL)
   assert.True(t, success)
}

调用htttest.NewServer方法,方法的第一个参数为一个函数类型,该函数的内容就是用户自定义的Web服务器行为。通过这种方式,httptest会创建一个http服务器并返回*httptest.Server类型的变量mockAuthServer来表示该服务器的基本信息,然后就可以通过将实际要访问的服务器地址替换为我们自己Mock的服务器的url(mockAuthServer.URL)来完成对第三方接口的mock。

尽管单纯使用httptest已经可以解决外部http调用的mock问题,但解决方式仍然不够优雅,在实际的项目中,我们还需要进行更深度的Mock。

现在我也找了很多测试的朋友,做了一个分享技术的交流群,共享了很多我们收集的技术文档和视频教程。
如果你不想再体验自学时找不到资源,没人解答问题,坚持几天便放弃的感受
可以加入我们一起交流。而且还有很多在自动化,性能,安全,测试开发等等方面有一定建树的技术大牛
分享他们的经验,还会分享很多直播讲座和技术沙龙
可以免费学习!划重点!开源的!!!
qq群号:310357728【暗号:csdn999】

3.非侵入式的打桩框架

gomonkey是一款打桩框架,打桩的意思就是创建一个模拟的目标结果对目标内容进行替换,并且不需要修改代码本身。gomonkey可以对函数、方法、全局变量、函数变量等打桩。

3.1 对函数打桩

上面我们举了一个使用httptest Mock第三方服务器接口的例子,但是在实际的开发中,我们一般不会这样在参数中传递外部服务的url,更多的可能是用读取配置文件的方式。

func GetAuthUrl() string {
   //解析配置文件,返回对应的结构体
   config := ParseConfig(configFilePath)
   //返回配置文件中AuthUrl的值
   return config.AuthUrl
}

我们定义了一个GetAuthUrl()方法用于获取第三方用户登陆服务的url。然后我们就可以将上面的权限验证代码稍作修改:

func ValidateUserAuth2(username, password string) bool {
   //注意这行,我们通过调用GetAuthUrl()方法来获取第三方接口url
   authUrl := GetAuthUrl()
   body, _ := json.Marshal(struct {
      Username string
      Password string
   }{username,
      password})
   request, _ := http.NewRequest(http.MethodPost, authUrl, bytes.NewReader(body))
   client := http.Client{}
   response, _ := client.Do(request)
   return response.StatusCode == http.StatusOK

最后在测试的时候使用gomonkey完成对GetAuthUrl()方法的打桩,返回我们Mock好的http服务器的url:

func TestValidateUserAuth2(t *testing.T) {
   //函数类型的变量,用户定义Web服务器行为
   var handler http.HandlerFunc = func(writer http.ResponseWriter, request *http.Request) {
        writer.WriteHeader(200)
        writer.Write(nil)
   }
   mockAuthServer := httptest.NewServer(handler)
   //1.对GetAuthUrl()方法进行打桩,返回mockAuthServer的url
   patches := gomonkey.ApplyFunc(GetAuthUrl, func() string {
      return mockAuthServer.URL
   })
   //2.在程序结束时恢复现场
   defer patches.Reset()
   success := ValidateUserAuth2("user", "passwd")
   assert.True(t, success)
}
  • gomonkey.ApplyFunc(target interface{}, double interface{})函数表示为一个函数打桩。
  • gomonkey.ApplyFunc(target interface{}, double interface{})的第一个参数为函数的名称;第二个参数为用户Mock的函数,该函数用户替换目标函数的行为。
  • 在执行patches.Reset()方法之后,gomonkey会还原现场信息,也就是不再为该函数打桩。

通过这种方式我们就可以在不侵入业务代码的时候实现单元测试。

3.2 对方法打桩

对方法的打桩使用方式和对函数打桩基本上是一样的,我们直接拿官方的例子来说明一下,就不再多做解释。

func TestApplyMethod(t *testing.T) {
    slice := fake.NewSlice()
    var s *fake.Slice
    Convey("for succ", t, func() {
        err := slice.Add(1)
        So(err, ShouldEqual, nil)
        //对方法打桩
        patches := gomonkey.ApplyMethod(reflect.TypeOf(s), "Add", func(_ *fake.Slice, _ int) error {
            return nil
        })
        defer patches.Reset()
        err = slice.Add(1)
        So(err, ShouldEqual, nil)
        err = slice.Remove(1)
        So(err, ShouldEqual, nil)
        So(len(slice), ShouldEqual, 0)
    })
}

gomonkey.ApplyMethod(target reflect.Type, methodName string, double interface{})表示对一个成员方法打桩。第一个参数为目标成员的类型;第二个参数为目标方法的名称;第三个参数为用户Mock的函数,该函数用户替换目标方法的行为,函数的参数和返回值都和被打桩方法保持一致。

注:目前gomonkey由于Golang反射的限制,不支持对私有成员方法打桩。

3.3 对全局变量打桩

var num = 1

func TestApplyGlobalVar(t *testing.T) {
   patches := gomonkey.ApplyGlobalVar(&num, 100)
   defer patches.Reset()
   assert.Equal(t, num, 100)
}
  • gomonkey.ApplyGlobalVar表示对一个变量打桩,第一个参数为变量指针,第二个参数为变量值。

4.数据库Mock

sqlmock是一个数据库操作Mock工具,它可以自定义SQL操作的行为,不需要连接真实的数据库,并且无需侵入代码逻辑。下面我们举几个例子:先定义一个Person结构体来和数据库中的person表相对应

type Person struct {
   Id   int64
   Name string
   Age  int
}

然后,写两个基本的数据库操作:INSERT操作和SELECT操作

//向person表中插入一条记录
func InsertPerson(db *sql.DB, person *Person) (int64, error) {
   sqlStr := "insert into person (name, age) values (?, ?)"
   result, err := db.Exec(sqlStr, person.Name, person.Age)
   if err != nil {
      return -1, err
   }
   lastInsertId, _ := result.LastInsertId()
   return lastInsertId, nil
}
//根据id查询对应的person记录
func QueryPersonById(db *sql.DB, id int64) (Person, error) {
   sqlStr := "select name, age from person where id = ?"
   result := Person{Id: id}
   rows, err := db.Query(sqlStr, id)
   if err != nil {
      return result, err
   }
   if rows.Next(){
      rows.Scan(&result.Name, &result.Age)
   }
   return result, nil
}

如果我们想要测试这两个方法的正确性,一般的做法可能就是连接真实的数据库然后去验证数据库的变化是否符合预期。这样我们的测试就直接依赖了外部的系统,sqlmock可以通过Mock数据库很好的解决这个问题。

func TestInsertPerson(t *testing.T) {
   db, mock, _ := sqlmock.New()
   mock.ExpectExec("insert into person").WillReturnResult(sqlmock.NewResult(100, 1))
   p := &Person{
      Name: "zhangsan",
      Age:  23,
   }
   id, err := InsertPerson(db, p)
   assert.Equal(t, id, int64(100))
   assert.Nil(t, err)
}
  • 通过sqlmock.New()方法我们可以得到一个*sql.DB结构体指针实例,我们在调用InsertPerson方法的时候只需要把真实数据库的连接替换为该结构体指针就可以实现对数据库的Mock。
  • WillReturnResult方法表示自定义的数据库行为,参数为driver.Result类型,可以通过sqlmock.NewResult方法来构造。sqlmock.NewResult(100,1)的第一个参数表示该语句上一次插入的记录id为100,这对设置了自增id的表很有用;第二个参数表示该表通过这条语句收到影响的记录条数为1。
     

不只正常结果的返回,sqlmock还可以Mock数据库操作的异常情况:

func TestInsertPersonWithError(t *testing.T) {
   db, mock, _ := sqlmock.New()
   mock.ExpectExec("insert into person").WillReturnError(errors.New("database internal error"))
   p := &Person{
      Name: "zhangsan",
      Age:  23,
   }
   id, err := InsertPerson(db, p)
   assert.Equal(t, id, int64(-1))
   assert.NotNil(t, err)
}
  • 用法也非常简单,只需要将WillReturnResult方法替换为WillReturnError方法就可以了。
  • 这个方式可以用来测试我们程序对异常情况的处理是否正确。

对于查询操作,sqlmock也可以很好的支持:

func TestQueryPerson(t *testing.T) {
   db, mock, _ := sqlmock.New()
   //构造返回记录
   rows := sqlmock.NewRows([]string{"name", "age"}).
      AddRow("zhangsan", 23)
   mock.ExpectQuery("select name, age from person").WillReturnRows(rows)
   person, err := QueryPersonById(db, 1)
   assert.Nil(t, err)
   assert.Equal(t, person.Name, "zhangsan")
   assert.Equal(t, person.Age, 23)
}
  • 使用sqlmock.NewRows(columns []string)方法可以构造数据库记录,参数为记录的字段。
  • 然后通过AddRow(values ...driver.Value)方法添加记录的内容。

5.让测试变得更简洁

在Golang的标准库中没有提供断言功能,这让我们平时的测试很不方便。testify的assert包提供了完善的断言功能,使用方式也非常简单。熟悉Java的同学应该不会对下面的形式感到陌生。

使用方式:

func TestSomething(t *testing.T) {
    // All these assertions pass
    assert.Equal(t, "hello", "hello", "Values are equal")
    assert.NotEqual(t, "hello", "world", "Values are different")
    assert.Contains(t, "hello", "el", "String contains other given string")
    assert.True(t, true, "Value is true")
    assert.False(t, false, "Value is false")

    // All these assertions fail
    assert.Equal(t, "hello", "world", "Values are equal")
    assert.NotEqual(t, "hello", "hello", "Values are different")
    assert.Contains(t, "hello", "y", "String contains other given string")
    assert.True(t, false, "Value is true")
    assert.False(t, true, "Value is false")
}

6.总结

httptest可以帮助我们完成对Web服务器的Mock,sqlmock可以完成对数据库的Mock,这两个工具基本可以帮助我们完成绝大部分外部系统的Mock工作。但是,实际中的代码逻辑、层次等等都是多变的,我们很多情况下不能够很好的将httptest或是sqlmock的入口注入到代码中,这时候我们就需要用gomonkey动态的Mock一些函数、方法或是变量来帮助我们开启httptest或sqlmock的入口。而testify则是帮助我们在繁琐的判断和错误处理中脱离出来的一大利器。在这几个测试库的帮助下,我们便可以写出一手优雅漂亮的测试代码了。

END今天的分享到此结束了!点赞关注不迷路!

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

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

相关文章

JS加密/解密之HOOK实战2

上一篇文章介绍了HOOK常规的应用场景,这篇我们讲一下HOOK其他原生函数。又是一个新的其他思路 很多时候,当我们想要某些网站的请求参数的时候,因为某些加密导致了获取起来很复杂。 这时候hook就十分方便了 源代码 var _JSON_Parse JSON.…

ShardingSphere数据分片之分表操作

1、概述 Apache ShardingSphere 是一款分布式的数据库生态系统, 可以将任意数据库转换为分布式数据库,并通过数据分片、弹性伸缩、加密等能力对原有数据库进行增强。 Apache ShardingSphere 设计哲学为 Database Plus,旨在构建异构数据库上…

基于ssm高校实验室管理系统的设计与实现论文

摘 要 互联网发展至今,无论是其理论还是技术都已经成熟,而且它广泛参与在社会中的方方面面。它让信息都可以通过网络传播,搭配信息管理工具可以很好地为人们提供服务。针对高校实验室信息管理混乱,出错率高,信息安全性…

(二)五种最新算法(SWO、COA、LSO、GRO、LO)求解无人机路径规划MATLAB

一、五种算法(SWO、COA、LSO、GRO、LO)简介 1、蜘蛛蜂优化算法SWO 蜘蛛蜂优化算法(Spider wasp optimizer,SWO)由Mohamed Abdel-Basset等人于2023年提出,该算法模型雌性蜘蛛蜂的狩猎、筑巢和交配行为&…

区块链实验室(29) - 关闭或删除FISCO日志

1. FISCO日志 缺省情况下,FISCO启动日志模块,日志记录的位置在节点目录中。以FISCO自带案例为例,4节点的FISCO网络,24个区块产生的日志大小,见下图所示。 2.关闭日志模块 当节点数量增大,区块高度增大时&…

CMake ‘3.10.2‘ was not found in PATH or by cmake.dir property.

在部署Yolov5到安卓端的过程中出现:CMake ‘3.10.2’ was not found in PATH or by cmake.dir property. 原因: cmake版本太高,需要安装低版本的cmake 最开始下载的是默认最高版本的cmake,默认是3.22.1,解决方案是,下载…

MarsEdit 5 for Mac(博客编辑软件) - 博客创作的完美拍档!

您是一位热爱写作和分享的博主吗?如果是的话,那么MarsEdit 5 for Mac将成为您创作之旅中的完美拍档!这款博客编辑软件为Mac用户提供了无与伦比的便捷和灵活性。 MarsEdit 5具有直观的界面和强大的功能,让您轻松管理和编辑多个博客…

使用 PyTorch 完全分片数据并行技术加速大模型训练

本文,我们将了解如何基于 PyTorch 最新的 完全分片数据并行 (Fully Sharded Data Parallel,FSDP) 功能用 Accelerate 库来训练大模型。 动机 🤗 随着机器学习 (ML) 模型的规模、大小和参数量的不断增加,ML 从业者发现在自己的硬件…

什么是网站劫持

网站劫持是一种网络安全威胁,它通过非法访问或篡改网站的内容来获取机密信息或者破坏计算机系统。如果您遇到了网站劫持问题,建议您立即联系相关的安全机构或者技术支持团队,以获得更专业的帮助和解决方案。

短视频矩阵系统多账号搭建技术源码(源头3年开发者技术独立搭建)

一、短视频账号矩阵系统源码搭建源码步骤: 1. 选择适合的云服务环境搭建虚拟机。这里以AWS为例,购买并配置相应数量的EC2实例以及相应的网络设置。 2. 根据需要搭建多个抖音、快手等平台的官方账号,并根据各个平台的要求和规则进行内容创作和…

Web漏洞扫描工具有哪些?使用教程讲解

作为网络安全工程师,了解并掌握各种Web漏洞扫描工具对于识别和防御网络威胁至关重要。以下是一些常用且广受推崇的Web漏洞扫描工具,它们覆盖了从自动扫描到深度定制的各种需求。希望你能用得到呢。 1. OWASP ZAP (Zed Attack Proxy) 原理:…

Selenium+Python自动化脚本环境搭建的全过程

*本文仅介绍环境的搭建,不包含任何脚本编写教程。 先整体说一下需要用到工具 1、Python环境(包括pip) 2、谷歌浏览器(包括对应的WebDriver) 详细步骤: 一、Python环境搭建 1、下载安装包 Python Relea…

BitComet(比特彗星)for Mac/Win:极速下载,畅享BT资源!

BitComet(比特彗星)是一款功能强大的BT下载客户端,专为Mac和Windows用户量身定制。它以极速下载、长效种子、磁盘缓存和边下边放等技术为特色,让您轻松畅享BT资源。 一、极速下载 BitComet(比特彗星)采用…

Oauth2.0 认证

目录 前言 1.介绍 2.Oauth2.0过程详解 3.Oauth 整合到 Spring Boot 实践 4.方法及配置详解: 总结 前言 Oauth2.0 是非常流行的网络授权表准,已经广泛应用在全球范围内,比较大的公司,如腾讯等都有大量的应用场景。 1.介绍 …

Selenium UI自动化实战过程记录

一.前言 1.1项目框架 项目如何使用框架: 本项目采用unitest框架 设计模式是如何应用:本项目采用pageobject设计模式 UI对象库思想 项目设计 一个模块(被测项目的页面)对应一个py文件及一个测试类(测试文件&#x…

Azure Machine Learning - 使用 Azure OpenAI 服务生成文本

使用 Azure OpenAI 服务生成文本 关注TechLead,分享AI全维度知识。作者拥有10年互联网服务架构、AI产品研发经验、团队管理经验,同济本复旦硕,复旦机器人智能实验室成员,阿里云认证的资深架构师,项目管理专业人士&…

快解析结合智邦国际使用教程

北京智邦国际软件技术有限公司,是经中华人民共和国工业和信息化部以及北京经济和信息化委员会评定和审核的双软企业,国家重点支持的高新技术企业。 十几年来致力于企业信息化,主要从事ERP、CRM、项目管理、人资管理、移动应用等企业管理软件的…

探索 SNMPv3 魔法:armbian系统安装snmp服务并通过SNMPV3进行连接控制

文章目录 说明SNMP服务的安装本机连接SNMPV3操作MIB Browser连接SNMPV3问题总结密码过短权限配置错误,导致OID不存在 说明 工具 建议尝试专业版ireasoning MIB brower,因为只有专业版支持SNMP v3的连接。当然,也可以尝试其他SNMP客户端工具 …

C++系列第七篇 数据类型下篇 - 复合类型(结构体、共用体及枚举)

系列文章 C 系列 前篇 为什么学习C 及学习计划-CSDN博客 C 系列 第一篇 开发环境搭建(WSL 方向)-CSDN博客 C 系列 第二篇 你真的了解C吗?本篇带你走进C的世界-CSDN博客 C 系列 第三篇 C程序的基本结构-CSDN博客 C 系列 第四篇 C 数据类型…

【MATLAB】辛几何模态分解分解+FFT+HHT组合算法

有意向获取代码,请转文末观看代码获取方式~也可转原文链接获取~ 1 基本定义 辛几何模态分解(CEEMDAN)是一种处理非线性和非平稳信号的适应性信号分解方法。通过在信号中加入白噪声,并多次进行经验模态分解(EMD&#…