通过Mock玩转Golang单元测试!

news2024/11/19 19:29:30

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今天的分享到此结束了!点赞关注不迷路!

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

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

相关文章

windows的cmd/PowerShell修改中文字符编码

1.简介 由于编程的时候经常会用到不同的编码,但是由于cmd/bat脚本和命令行默认的编码不一致,就导致了输出的中文乱码,使用chcp命令可以修改当前命令行的编码方式 2.查看与修改 2.1.查看 chcp 常用编码编号 65001:UTF-8936&am…

Python-pdf工具自制(合并、拆分、删除)

pdf工具,之前写的合并工具有点麻烦,使用PyQt5库重写合并拆分和删除指定页面的程序 实现如图: 代码: import sysimport osfrom PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, QVBoxLayout, QWidget, QFileDia…

P10 Linux进程编程 fork创建子进程

目录 前言 01 fork()创建子进程 示例 1使用 fork()创建子进程。 02 fork创建新进程时发生了什么事? 2.1 父、子进程中对应的文件描述符指向了相同的文件表 前言 🎬 个人主页:ChenPi 🐻推荐专栏1: 《Linux C应用编程&#xf…

SystemVerilog学习(0)——目录与传送门

一、验证导论 SystemVerilog学习(1)——验证导论-CSDN博客文章浏览阅读403次。SystemVerilog自学,验证系统概述,什么是SVhttps://blog.csdn.net/apple_53311083/article/details/133953016 二、数据类型 SystemVerilog学习&…

hive自定义函数及案例

一.自定义函数 1.Hive自带了一些函数,比如:max/min等,但是数量有限,自己可以通过自定义UDF来方便的扩展。 2.当Hive提供的内置函数无法满足你的业务处理需要时,此时就可以考虑使用用户自定义函数。 3.根据用户自定义…

文献速递:多模态影像组学文献分享:多模态图注意力网络用于COVID-19预后预测

文献速递:多模态影像组学文献分享:多模态图注意力网络用于COVID-19预后预测 01 文献速递介绍 在处理像 COVID-19 这样的新出现的疾病时,患者和疾病特定因素(例如,体重或已知共病)对疾病的即时进展的影响…

python数据分析总结(pandas)

目录 前言 df导入数据 df基本增删改查 数据清洗 ​编辑 索引操作 数据统计 行列操作 ​编辑 df->types 数据格式化 ​编辑 日期数据处理 前言 此篇文章为个人python数据分析学习总结,总结内容大都为表格和结构图方式,仅供参考。 df导入数…

Python 从入门到精通 学习笔记 Day03

Python 从入门到精通 第三天 今日目标 流程控制语句、退出循环、练习学习的内容 一、流程控制语句 流程控制的三种方式:顺序语句、双分支语句、循环语句 双分支语句 Python 的双分支语句使用if-else语句实现。 其语法结构如下: if条件:#如果条作为真&#xff…

stm32 使用18B20 测试温度

用18b20 测试温度是非常常用的,不过18B20的调试不是这么容易的,有些内容网上很多的,不再重复说了,我先把波形说一下,再说程序部分: 整个都温度数据的顺序是: 1.700uS的低电平复位并测试18B20的…

使用MetaMask + Ganache搭建本地私有网络并实现合约部署与互动

我使用Remix编写合约,MetaMask钱包工具和Ganache搭建了一个私有网络,并且实现了合约的部署和互动。 在前面的博客中提到了 Remix在线环境及钱包申请 以及 Solidity的基本语法 ,没看过的小伙伴可以点击链接查看一下,都是在本专栏下…

Swift 如何实现自定义 Tab Bar

前言 每个 UI 设计师都喜欢美丽而有动画效果的 Tab Bar。然而,对于开发人员来说,实现这种设计可能是一场噩梦。当然,使用 Apple 的原生 Tab Bar 组件并专注于更有趣的事情,比如业务逻辑的实现,会更容易。但如果我们必…

CPU、MCU、MPU、DSP、FPGA各是什么?有什么区别?

1、CPU 中央处理器,简称 CPU(Central Processing Unit),中央处理器主要包括两个部分,即控制器、运算器,其中还包括高速缓冲存储器及实现它们之间联系的数据、控制的总线。 电子计算机三大核心部件就是CPU…

了解c++11中的新增

一,统一的初始化列表 在引入c11后,我们得出计划都可以用初始化列表进行初始化。 C11 扩大了用大括号括起的列表 ( 初始化列表 ) 的使用范围,使其可用于所有的内置类型和用户自 定义的类型, 使用初始化列表时,可添加等…

JS基础之原型原型链

JS基础之原型&原型链 原型&原型链构造函数创建对象prototypeprotoconstructor实例与原型原型的原型原型链其他constructorproto继承 原型&原型链 构造函数创建对象 我们先使用构造函数创建一个对象: function Person(){ } var person new Person();…

【数据结构和算法】到达首都的最少油耗

其他系列文章导航 Java基础合集数据结构与算法合集 设计模式合集 多线程合集 分布式合集 ES合集 文章目录 其他系列文章导航 文章目录 前言 一、题目描述 二、题解 三、代码 四、复杂度分析 前言 这是力扣的2477题,难度为中等,解题方案有很多种&…

tomcat源码学习记录

tomcat 学习记录 tomcat 编译ant 下载编译运行 源码Debug运行 Bootstrap运行Tomcat查看状态 pom.xml测试EmbeddedTomcat 参考书籍博客 tomcat 编译 下载 tomcat 10 源码,解压然后idea导入 包存放的默认位置如下:base.path${user.home}/tomcat-build-lib…

Linux升级nginx版本

处于漏洞修复目的服务器所用nginx是1.16.0版本扫出来存在安全隐患,需要我们升级到1.17.7以上。 一般nginx默认在 /usr/local/ 目录,这里我的nginx是自定义的路径安装在 /app/weblogic/nginx 。 1.查看生产环境nginx版本 cd /app/weblogic/nginx/sbin/…

class065 A星、Floyd、Bellman-Ford与SPFA【算法】

class065 A星、Floyd、Bellman-Ford与SPFA【算法】 2023-12-9 19:27:02 算法讲解065【必备】A星、Floyd、Bellman-Ford与SPFA code1 A*算法模版 // A*算法模版(对数器验证) package class065;import java.util.PriorityQueue;// A*算法模版&#xff…

Elon Musk艾隆・马斯克的聊天机器人Grok上线可以使用啦,为X Premium Plus订阅者推出

艾隆・马斯克旗下的 AI 初创公司X(前身“推特”)开发的 ChatGPT 竞争对手 Grok 已经在 X 平台上正式推出。Grok 是一个基于生成模型 Grok-1的聊天机器人,它能够回答问题并提供最新的信息。与其他聊天机器人不同,Grok 可以实时获取…

luceda ipkiss教程 44:在PyCharm 中设置Template text

通过设置Template text,可以提升写代码的速度和版图设计效率。 设置了Template text,在PyCharm 命令窗口输入i3后按enter建,就可以快速输入 from ipkiss3 import all as i3 这一段代码,使用起来也是非常方便: 设置过程…