通过Mock玩转Golang单元测试!

这篇具有很好参考价值的文章主要介绍了通过Mock玩转Golang单元测试!。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

通过Mock玩转Golang单元测试!,软件测试,安全测试,自动化测试,golang,单元测试,log4j,selenium,测试工具,压力测试

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】

通过Mock玩转Golang单元测试!,软件测试,安全测试,自动化测试,golang,单元测试,log4j,selenium,测试工具,压力测试

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今天的分享到此结束了!点赞关注不迷路!文章来源地址https://www.toymoban.com/news/detail-755491.html

到了这里,关于通过Mock玩转Golang单元测试!的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 软件测试技术(单元测试)

    1、JUnit JUnit是一个Java语言的单元测试框架,用于编写和运行测试。它提供了一些注解和断言方法,可以使测试代码更加简洁和易于阅读。使用JUnit进行单元测试,可以提高代码的质量和可维护性,减少代码的错误和缺陷,从而提高整个系统的稳定性和可靠性。 JUnit框架的核心

    2024年02月04日
    浏览(53)
  • 软件测试系列--单元测试

     一、单元测试的概念 单元测试(Unit Testing)是对软件基本组成单元进行的测试,如函数(function或procedure)或一个类的方法(method)。当然这里的基本单元不仅仅指的是一个函数或者方法,有可能对应多个程序文件中的一组函数。 单元也具有一些基本的属性。比如:明确的

    2024年02月16日
    浏览(40)
  • 【软件测试】单元测试

    单元测试 (Unit Testing),又称 单体测试 、 模块测试 ,是最小单位的测试,其依据是详细设计、程序源代码或编码标准,对模块内所有重要的控制路径设计测试用例,以便发现模块内部的错误,使代码达到设计要求。 单元测试是所有的测试活动中最早进行的,它能以最低的成

    2024年02月13日
    浏览(41)
  • 软件测试之单元测试详解

    目录 前言: 1、什么是单元测试? 2、什么是好的单元测试? 3、怎么写单元测试? 4、玩转单元测试 单元测试是软件测试中的一种测试方法,用于验证代码中最小可测试单元的正确性。它主要关注对程序的各个独立模块、函数或方法进行测试,以确保其按照预期产生正确的输

    2024年02月12日
    浏览(55)
  • 软件测试之【单元测试、系统测试、集成测试】

    目录 一、单元测试的概念 二、单元测试的目的 三、单元的常见错误 四、如何进行单元测试 五、单元测试策略 六、系统测试的概念 七、系统测试的环境 八、系统测试的类型 九、系统测试的过程 十、集成测试概念 十一、集成测试的目的 十二、集成测试关注的重点 十三、集

    2023年04月08日
    浏览(49)
  • 软件测试之单元测试、系统测试、集成测试详解

    🍅 视频学习: 文末有免费的配套视频可观看 🍅 关注公众号【互联网杂货铺】,回复 1 , 免费获取软件测试全套资料,资料在手,涨薪更快 单元测试是对软件基本组成单元进行的测试,如函数或一个类的方法。当然这里的基本单元不仅仅指的是一个函数或者方法,有可能

    2024年04月25日
    浏览(55)
  • 软件测试实验:Junit单元测试

    目录 前言 实验目的 实验内容 实验要求 实验过程 题目一 题目一测试结果 题目二 题目二实验结果 总结 软件测试是软件开发过程中不可缺少的一个环节,它可以保证软件的质量和功能,发现并修复软件的缺陷和错误。软件测试分为多种类型,其中一种是单元测试,即对软件

    2024年02月02日
    浏览(51)
  • 【软件测试】单元测试、集成测试、系统测试有什么区别?

    1、粒度不同 集成测试bai粒度居中,单元测试粒度最小,系统du测试粒度最大。 2、测试方式不同 集成测试一般由开发zhi小组采用白盒加黑盒的方式来测试,单元测试一般由开发小组采用白盒方式来测试,系统测试一般由独立测试小组采用黑盒方式来测试。 3、测试内容不同

    2024年02月09日
    浏览(50)
  • 【软件测试】学习笔记-如何做好单元测试

    在正式开始今天的话题之前,我先给你分享一个工厂生产电视机的例子。 工厂首先会将各种电子元器件按照图纸组装在一起构成各个功能电路板,比如供电板、音视频解码板、射频接收板等,然后再将这些电路板组装起来构成一个完整的电视机。 如果一切顺利,接通电源后

    2024年02月03日
    浏览(64)
  • 软件测试--应用JUnit进行单元测试

    JUnit是一个开源的Java编程语言的单元测试框架,最初由 Erich Gamma 和 Kent Beck 编写。Junit测试是一种白盒测试工具。JUnit是一套框架,继承TestCase类,就可以用Junit进行自动测试了。具有JUnit经验对于应用“测试驱动开发(TDD)”的程序开发模型是非常重要的。 JUnit本质上是一套框

    2023年04月12日
    浏览(42)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包