首页 > 其他分享 >在Golang中使用Testify mock框架

在Golang中使用Testify mock框架

时间:2023-04-27 17:34:08浏览次数:45  
标签:lang return string theDBMock Testify Golang FetchMessage mock

1. 前言

我使用golang已经有一段时间了,但直到最近我才终于明白如何在golang测试中进行对象mocking。由于我来自Java,所以在golang中mock对象的方式对我来说并不清楚。这篇文章是我如何达到目前理解的自我记录。在这篇文章中,我使用了来自Testify的mock功能 在阅读这篇这篇文章之前, 读者需要有golang基础。

首先, 我们来创建一个非常简单的服务,如下所示:

GreeterService是一个向用户打招呼的服务。其由两种问候方式:
Greet()根据设置的语言向用户打招呼
GreetDefaultMessage()将使用默认消息向用户打招呼致意,不涉及到语言设置.
在GreeterService内部,Greet()将调用db.FetchMessage(lang),GreetDefaultMessage()将呼叫db.FetchDefaultMessage()。我们可以在真实场景想象的样子,db类是调用真实数据库的类。因此,我们需要在测试中使用mock来避免测试调用实际的数据库。golang中没有class的概念,但我们可以认为struct行为与类是等效的。

2. 实现代码

首先我们定义一个名为service包。然后,我们将创建一个dv结构及其接口,并将其命名为db。

DB.go

package service

type db struct{}

// DB is fake database interface.
type DB interface {
    FetchMessage(lang string) (string, error)
    FetchDefaultMessage() (string, error)
}

然后我们将创建GreeterService接口和实现一个调用DB接口的greeter struct。greeter struct构造函数第二个参数接收lang参数。


type greeter struct {
    database DB
    lang     string
}

// GreeterService is service to greet your friends.
type GreeterService interface {
    Greet() string
    GreetInDefaultMsg() string
}

为了使数据库结构实现数据库接口,我们将添加所需的方法,并使用指针接收者。


func (d *db) FetchMessage(lang string) (string, error) {
    // in real life, this code will call an external db
    // but for this sample we will just return the hardcoded example value
    if lang == "en" {
        return "hello", nil
    }
    if lang == "es" {
        return "holla", nil
    }
    return "bzzzz", nil
}

func (d *db) FetchDefaultMessage() (string, error) {
    return "default message", nil
}

接下来,我们需要实现greeter的方法Greet()和GreetInDefaultMsg()。


func (g greeter) Greet() string {
    msg, _ := g.database.FetchMessage(g.lang) // call database to get the message based on the lang
    return "Message is: " + msg
}

func (g greeter) GreetInDefaultMsg() string {
    msg, _ := g.database.FetchDefaultMessage() // call database to get the default message
    return "Message is: " + msg
}

上面,greetiner方法将会调用DB以获取实际消息。
为Greeter和DB创建一个工厂方法用于创建greeter和db实例。


func NewDB() DB {
    return new(db)
}

func NewGreeter(db DB, lang string) GreeterService {
    return greeter{db, lang}
}

在实现的最后一部分,我们将编写一个主函数来运行服务。

package main

import (
    "fmt"
    "testify-mock/service"
)

func main() {
    d := service.NewDB()

    g := service.NewGreeter(d, "en")
    fmt.Println(g.Greet()) // Message is: hello
    fmt.Println(g.GreetInDefaultMsg()) // Message is: default message

    g = service.NewGreeter(d, "es")
    fmt.Println(g.Greet()) // Message is: holla

    g = service.NewGreeter(d, "random")
    fmt.Println(g.Greet()) // Message is: bzzzz
}

运行后的输出如下。


$ go run main.go
Message is: hello
Message is: default message
Message is: holla
Message is: bzzzz

3. Mock和测试

在上面的实现完成后, 我们将编写一个测试并模拟DB行为. 如前所述,我们希望在运行测试时防止调用实际数据库.

为了实现这个目标, 我们将mock DB接口. 不幸的是, 在golang中创建模拟对象并不像在Java中那样直截了当. 在Java中使用mockito, mocking可以像下面这样这么简单:


GreetingService mock = Mockito.mock(GreetingService.class);

但是在golang中, 我们需要创建一个新的结构体并将testify模拟对象嵌入其中, 如下所示:


type dbMock struct {
    mock.Mock
}

然后, 为了使该模拟对象符合DB接口, 我们还需要手动实现接口的所有方法. 还有一个指定方法我们需要去调用它.
第一: 如果被模拟的方法有参数的话, 我们需要调用Mock.Called(args)接收参数

第二: 调用的放回值将用作我们要模拟的方法的返回值. 两种方法都返回(string, error).

因此, 模拟方法的返回语句是return args.String(0), args.Error(1).

返回语句的规则是args.<ReturnValueType>(<index>). 索引从零开始


func (d *dbMock) FetchMessage(lang string) (string, error) {
    args := d.Called(lang)
    return args.String(0), args.Error(1)
}

func (d *dbMock) FetchDefaultMessage() (string, error) {
    args := d.Called()
    return args.String(0), args.Error(1)
}

按照这个规则, 假设我们要模拟的方法的返回值类型为(int, string, bool), 那么我们在mock方法返回值时需要这样写 return args.Int(0), args.String(1), args.Bool(2)

如果返回值类型中有复杂类型, 如结构体, 接口之类的, 那么return语句应该像这样写.

return args.Get(0).(*MyObject), args.Get(1).(*AnotherObjectOfMine)

至此我们几乎创建了mockito一句完成的所有内容, 注意这里的用词是几乎, 这里只是创建了struct, 再创建一个实例对象, 就完全完成了mockito帮助我们完成的所有工作.

这些mock步骤都是机械式的, 好在golang帮我实现了一些自动化工具, 这些mock代码也不用我们自己动手写. 手写的过程对于我们理解mock的底层原理还是有帮助的.

有关go mock更多额外信息,请参阅testift go doc

4. Mock无参方法

在上一节中, 我们创建了一个DB的mock struct, 现在我们可以在测试中使用它了.在这一节中, 我们将了解如何使用Testify模拟不带参数的方法.

在DB interface上有一个不带参数的方法FetchDefaultMessage, 我们想要在测试中模拟它. 我们可以像下面这样创建一个模拟对象:


func TestMockMethodWithoutArgs(t *testing.T) {
    theDBMock := dbMock{} // create the mock
    theDBMock.On("FetchDefaultMessage").Return("foofofofof", nil) // mock the expectation
    g := greeter{&theDBMock, "en"} // create greeter object using mocked db
    assert.Equal(t, "Message is: foofofofof", g.GreetInDefaultMsg()) // assert what actual value that will come
    theDBMock.AssertNumberOfCalls(t, "FetchDefaultMessage", 1) // we can assert how many times the mocked method will be called
    theDBMock.AssertExpectations(t) // this method will ensure everything specified with On and Return was in fact called as expected
}

在上面的代码中, 我们创建了一个dbMock对象, 并使用On方法指定了要模拟的方法FetchDefaultMessage().
然后, 我们使用Return方法指定了模拟方法的返回值. 当该方法被调用时, 将返回我们指定的模拟值.

5. Mock带参数的方法

在上一节中, 我们已经了解了如何模拟没有参数的方法. 在这一节中, 我们将学习如何模拟带有参数的方法.

在DB interface上有一个带参数的方法FetchMessage(lang string), 我们想要在测试中模拟它. 我们可以像下面这样创建一个模拟对象:


func TestMockMethodWithArgs(t *testing.T) {
    theDBMock := dbMock{}
    theDBMock.On("FetchMessage", "sg").Return("lah", nil) // if FetchMessage("sg") is called, then return "lah"
    g := greeter{&theDBMock, "sg"}
    assert.Equal(t, "Message is: lah", g.Greet())
    theDBMock.AssertExpectations(t)
}

在上面的代码中, 我们创建了一个dbMock对象, 并使用On方法指定了要模拟的方法FetchMessage和参数"sg". 然后我们使用Return方法指定了模拟方法的返回值. 现在当我们在测试中调用FetchMessage("sg")方法时, 将返回我们指定的模拟值.

如果我们想要校验实际参数是否与预期相同, 我们可以使用AssertExpectations方法.

6. Mock带参数的方法, 但是参数具体内容非测试重点

有时我们想模拟一个方法,但我们不在乎传递的实际参数。为此,我们可以在On()方法参数后面的第二个参数中使用mock.Anything。


func TestMockMethodWithArgsIgnoreArgs(t *testing.T) {
    theDBMock := dbMock{}
    theDBMock.On("FetchMessage", mock.Anything).Return("lah", nil) // if FetchMessage(...) is called with any argument, please also return lah
    g := greeter{&theDBMock, "in"}
    assert.Equal(t, "Message is: lah", g.Greet())
    theDBMock.AssertCalled(t, "FetchMessage", "in")
    theDBMock.AssertNotCalled(t, "FetchMessage", "ch")
    theDBMock.AssertExpectations(t)
    mock.AssertExpectationsForObjects(t, &theDBMock)
}

7. Mock带参数的方法, 并校验实际参数

有时我们需要模拟一个具有复杂参数的方法,但希望根据参数的某些属性或从中进行计算来匹配mock。例如,我们想模仿FetchMessage方法,但前提是lang参数以字母i开头。

func TestMatchedBy(t *testing.T) {
    theDBMock := dbMock{}
    theDBMock.On("FetchMessage", mock.MatchedBy(func(lang string) bool { return lang[0] == 'i' })).Return("bzzzz", nil) // all of these call FetchMessage("iii"), FetchMessage("i"), FetchMessage("in") will match
    g := greeter{&theDBMock, "izz"}
    msg := g.Greet()
    assert.Equal(t, "Message is: bzzzz", msg)
    theDBMock.AssertExpectations(t)
}

8. Mockery

正如我们在上一节中所看到的,在进行实际测试和模拟行为之前,我们需要手动创建mock结构体。Mockery可以帮助我们摆脱手工劳动。
首先,我们只需要安装Mockery:

go get github.com/vektra/mockery/.../

然后生成mock

mockery -name <interfaceToMock>

将生成一个包含现成mock的文件,我们可以将其添加到测试中。
本文中示例代码的完整来源可在此gitlab仓库中获得。

9. 参考

Mocking in Golang Using Testify

标签:lang,return,string,theDBMock,Testify,Golang,FetchMessage,mock
From: https://www.cnblogs.com/guoapeng/p/17359565.html

相关文章

  • 还不知道怎么 Mock ,用这 6款工具!
    以下是几个常用的国外可以mock测试的工具,供参考:MockServer:MockServer是一个开源的APImock测试工具,提供了强大的模拟服务器和mock服务功能。MockServer支持多种语言和格式,包括Java、.NET、REST、SOAP等。WireMock:WireMock是一个轻量级的HTTPmock测试工具,可以......
  • Golang单元测试
    1.前言2.先决条件3.创建单元测试的示例程序4.创建单元测试5.使用gotest运行测试6.Table-driven的单元测试7.测试覆盖率8.Go基准测试9.为代码写示例10.总结11.参考文档1.前言原文:HowToWriteUnitTestsinGoAuthor:TobiBalogun译者:philoenglis......
  • golang1.6版本json包解析嵌套指针的问题小记
    指针的指针问题本地跑的好好的,测试环境跑的好好,预发布环境(准线上环境),跪了。起因就是:1a:=&struct{s:""}2json.Unmarshal([]byte{},&a)3fmt.Println(a.s)//报错行第一行代码进行&取地址,获得指针变量。第二行代码,进行json解析的时候,传入了&a, 指针的指针,a到了jso......
  • Golang 并发&同步的详细原理和使用技巧
    Golang并发概要说明并发模型Golang的并发模型属于一种很典型的CSP(communicatingsequentialprocesses)并发模型,其核心是不要通过共享内存来通信,而应该通过通信来共享内存。具体实现,就是通过goroutine来实现并发,然后并发的goroutine之间通过Channel来进行通信;为此,Gola......
  • Golang - 5 Golang的流程控制:if/else、for、switch
    5流程控制目录5流程控制1if/else1.1语法2for2.1语法2.2简单写法与实现while的功能2.3基于迭代的循环、基于索引的循环3switch3.1switch的基本使用3.2各种形式1if/else1.1语法 //基本形式if条件1{ }else条件2{ }else{ }多个分支age:=......
  • golang -WARNING: undefined behavior - version of Delve is too old for Go version
    1.背景启动警告 这是idea内置的dlv.exe调试器版本太低了2.解决安装最新的goinstallgithub.com/go-delve/delve/cmd/dlv@latest安装成功后,在golang的安装位置多出来个新的dlv.exe  idea打开配置 写上自己的地址即可下面是我的 重启idea生效......
  • 关于golang线程安全
    最近在字节面试,面试有一个提问:golang中的string赋值是线程安全的吗?如果是,怎么验证,如果不是,怎么验证第一反应,golang的string底层结构:typestringStructstruct{strunsafe.Pointerlenint}其中str是一个不变数组,所以该变字符串的内容都会重新生成一个底层数组,但......
  • 引用 maxmind golang 库导致的程序无法 recover crash 的问题
    新做的Gateway程序打算使用一个maxmind第三方库来解析地理信息,想了一下比较简单找了一个库直接使用。项目跑了一天得到了一堆panic,程序崩溃超过1s丢了不少数据。 从stack信息可以看到调用amxminddb-golang这个库的readLeft出现了错误,最后抛出了一个unexceptedf......
  • golang 使用 net包实现 tcp server 示例
    之前用到golang进行网络编程时,主要就是使用net/http和web框架gin,这些网络库的底层其实也还是用的标准库自带的net包,很多是对路由或者其他做封装,而且golang本身的长处之一也是网络IO的处理,这也得益于其底层的IO模型,今天我们分享的是基于TCPserver/client的简单实现,后......
  • Golang - time包获取当前日期/时间戳并格式化输出
    获取时间对象packagemainimport("fmt""time")//24小时制constLAYOUT="2006-01-0215:04:05"//只能用这个日期,据说这是golang的诞生时间funcmain(){//获取当前日期now:=time.Now()fmt.Printf(now.Format(LAYOUT))//2022......