Go中的模拟函数

pgpifvop  于 11个月前  发布在  Go
关注(0)|答案(9)|浏览(174)

我对依赖关系感到困惑。我希望能够用mock函数调用替换一些函数调用。下面是我的代码片段:

func get_page(url string) string {
    get_dl_slot(url)
    defer free_dl_slot(url)
    
    resp, err := http.Get(url)
    if err != nil { return "" }
    defer resp.Body.Close()
    
    contents, err := ioutil.ReadAll(resp.Body)
    if err != nil { return "" }
    return string(contents)
}

func downloader() {
    dl_slots = make(chan bool, DL_SLOT_AMOUNT) // Init the download slot semaphore
    content := get_page(BASE_URL)
    links_regexp := regexp.MustCompile(LIST_LINK_REGEXP)
    matches := links_regexp.FindAllStringSubmatch(content, -1)
    for _, match := range matches{
        go serie_dl(match[1], match[2])
    }
}

字符串
我希望能够测试downloader(),而不实际通过http获取页面-即通过模拟get_page(更容易,因为它只返回字符串形式的页面内容)或http.Get()
我发现这个线程似乎是关于一个类似的问题。朱利安菲利普斯提出了他的库,Withmock作为一个解决方案,但我无法让它工作。这里是我的测试代码的相关部分,这主要是货物邪教代码给我,说实话:

import (
    "testing"
    "net/http" // mock
    "code.google.com/p/gomock"
)
...
func TestDownloader (t *testing.T) {
    ctrl := gomock.NewController()
    defer ctrl.Finish()
    http.MOCK().SetController(ctrl)
    http.EXPECT().Get(BASE_URL)
    downloader()
    // The rest to be written
}


测试输出如下:
错误:无法安装'_et/http':exit status 1输出:无法加载包:package _et/http:在中找到包http(chunked.go)和main(main_mock.go)
/var/folders/z9/ql_yn5h550s6shtb9c5sggj40000gn/T/withmock570825607/path/src/_et/http
Withmock可以解决我的测试问题吗?我应该怎么做才能让它工作?

vnzz0bqm

vnzz0bqm1#

就我个人而言,我不使用gomock(或任何mocking框架;没有它,Go中的mocking非常容易)。我要么将依赖项作为参数传递给downloader()函数,要么将downloader()作为一个类型上的方法,类型可以保存get_page依赖项:

方法一:将get_page()作为downloader()的参数传入

type PageGetter func(url string) string

func downloader(pageGetterFunc PageGetter) {
    // ...
    content := pageGetterFunc(BASE_URL)
    // ...
}

字符串

主要内容:

func get_page(url string) string { /* ... */ }

func main() {
    downloader(get_page)
}

测试:

func mock_get_page(url string) string {
    // mock your 'get_page()' function here
}

func TestDownloader(t *testing.T) {
    downloader(mock_get_page)
}

方法2:将download()设为Downloader类型的方法:

如果你不想把依赖项作为参数传递,你也可以让get_page()成为一个类型的成员,让download()成为该类型的方法,然后它可以使用get_page

type PageGetter func(url string) string

type Downloader struct {
    get_page PageGetter
}

func NewDownloader(pg PageGetter) *Downloader {
    return &Downloader{get_page: pg}
}

func (d *Downloader) download() {
    //...
    content := d.get_page(BASE_URL)
    //...
}

主要内容:

func get_page(url string) string { /* ... */ }

func main() {
    d := NewDownloader(get_page)
    d.download()
}

测试:

func mock_get_page(url string) string {
    // mock your 'get_page()' function here
}

func TestDownloader() {
    d := NewDownloader(mock_get_page)
    d.download()
}

up9lanfz

up9lanfz2#

如果您更改函数定义以使用变量:

var get_page = func(url string) string {
    ...
}

字符串
您可以在测试中覆盖它:

func TestDownloader(t *testing.T) {
    get_page = func(url string) string {
        if url != "expected" {
            t.Fatal("good message")
        }
        return "something"
    }
    downloader()
}


不过要小心,如果其他测试测试的是您重写的函数的功能,那么它们可能会失败!
Go语言的作者在Go语言标准库中使用这种模式来将测试钩子插入代码中,以使测试变得更容易:

8ehkhllq

8ehkhllq3#

我使用了一种稍微不同的方法,其中publicstruct方法实现了interface,但它们的逻辑仅限于 Package private(未导出)函数,这些函数将这些interface作为参数。这为您提供了模拟几乎任何依赖项所需的粒度,同时还拥有一个干净的API,可以从测试套件外部使用。
要理解这一点,必须理解您可以访问测试用例中未导出的方法(即从您的_test.go文件中),因此您可以测试这些方法,而不是测试导出的方法,这些方法除了 Package 之外没有任何逻辑。
总结:测试未导出的函数,而不是测试导出的函数!
让我们举个例子。假设我们有一个Slack API结构体,它有两个方法:

  • SendMessage方法,用于向Slack webhook发送HTTP请求
  • SendDataSynchronously方法给出了一个字符串切片,它迭代这些字符串,并在每次迭代时调用SendMessage

因此,为了测试SendDataSynchronously而不每次都发出HTTP请求,我们必须模拟SendMessage,对吗?

package main

import (
    "fmt"
)

// URI interface
type URI interface {
    GetURL() string
}

// MessageSender interface
type MessageSender interface {
    SendMessage(message string) error
}

// This one is the "object" that our users will call to use this package functionalities
type API struct {
    baseURL  string
    endpoint string
}

// Here we make API implement implicitly the URI interface
func (api *API) GetURL() string {
    return api.baseURL + api.endpoint
}

// Here we make API implement implicitly the MessageSender interface
// Again we're just WRAPPING the sendMessage function here, nothing fancy 
func (api *API) SendMessage(message string) error {
    return sendMessage(api, message)
}

// We want to test this method but it calls SendMessage which makes a real HTTP request!
// Again we're just WRAPPING the sendDataSynchronously function here, nothing fancy
func (api *API) SendDataSynchronously(data []string) error {
    return sendDataSynchronously(api, data)
}

// this would make a real HTTP request
func sendMessage(uri URI, message string) error {
    fmt.Println("This function won't get called because we will mock it")
    return nil
}

// this is the function we want to test :)
func sendDataSynchronously(sender MessageSender, data []string) error {
    for _, text := range data {
        err := sender.SendMessage(text)

        if err != nil {
            return err
        }
    }

    return nil
}

// TEST CASE BELOW

// Here's our mock which just contains some variables that will be filled for running assertions on them later on
type mockedSender struct {
    err      error
    messages []string
}

// We make our mock implement the MessageSender interface so we can test sendDataSynchronously
func (sender *mockedSender) SendMessage(message string) error {
    // let's store all received messages for later assertions
    sender.messages = append(sender.messages, message)

    return sender.err // return error for later assertions
}

func TestSendsAllMessagesSynchronously() {
    mockedMessages := make([]string, 0)
    sender := mockedSender{nil, mockedMessages}

    messagesToSend := []string{"one", "two", "three"}
    err := sendDataSynchronously(&sender, messagesToSend)

    if err == nil {
        fmt.Println("All good here we expect the error to be nil:", err)
    }

    expectedMessages := fmt.Sprintf("%v", messagesToSend)
    actualMessages := fmt.Sprintf("%v", sender.messages)

    if expectedMessages == actualMessages {
        fmt.Println("Actual messages are as expected:", actualMessages)
    }
}

func main() {
    TestSendsAllMessagesSynchronously()
}

字符串
我喜欢这种方法的原因是,通过查看未导出的方法,您可以清楚地看到依赖项是什么。同时,您导出的API更清晰,需要传递的参数更少,因为这里真正的依赖项只是父接收器,它实现了所有这些接口本身。然而,每个函数都可能只依赖于其中的一部分(一个,也许是两个接口),这使得重构变得容易得多。很高兴看到你的代码是如何真正耦合的,只要看看函数签名,我认为它是一个强大的工具,可以防止闻到代码。
为了方便起见,我将所有内容都放在一个文件中,以便您在playground here中运行代码,但我建议您也在GitHub上查看完整的示例,这里是slack.go文件,这里是slack_test.go
here整个事情。

bz4sfanl

bz4sfanl4#

我会这样做,

主要

var getPage = get_page
func get_page (...

func downloader() {
    dl_slots = make(chan bool, DL_SLOT_AMOUNT) // Init the download slot semaphore
    content := getPage(BASE_URL)
    links_regexp := regexp.MustCompile(LIST_LINK_REGEXP)
    matches := links_regexp.FindAllStringSubmatch(content, -1)
    for _, match := range matches{
        go serie_dl(match[1], match[2])
    }
}

字符串

测试

func TestDownloader (t *testing.T) {
    origGetPage := getPage
    getPage = mock_get_page
    defer func() {getPage = origGatePage}()
    // The rest to be written
}

// define mock_get_page and rest of the codes
func mock_get_page (....


我会避免在golang中使用_。最好使用camelCase

c9qzyr3d

c9qzyr3d5#

最简单的方法是将函数设置为全局变量,并在测试之前设置自定义方法

// package base36

func GenerateRandomString(length int) string {
    // your real code
}

// package teamManager

var RandomStringGenerator = base36.GenerateRandomString

func (m *TeamManagerService) CreateTeam(ctx context.Context) {
 
    // we are using the global variable
    code = RandomStringGenerator(5)
 
    // your application logic

    return  nil
}

字符串
在测试中,必须首先模拟该全局变量,

teamManager.RandomStringGenerator = func(length int) string {
        return "some string"
    }
    
   service := &teamManager.TeamManagerService{}
   service.CreateTeam(context.Background())
   // now when we call any method that user teamManager.RandomStringGenerator, it will call our mocked method

另一种方法是将RandomStringGenerator作为依赖项传递,并将其存储在TeamManagerService中,并像这样使用它:

// package teamManager

type TeamManagerService struct {
   RandomStringGenerator func(length int) string
}

// in this way you don't need to change your main/where this code is used
func NewTeamManagerService() *TeamManagerService {
    return &TeamManagerService{RandomStringGenerator: base36.GenerateRandomString}
}

func (m *TeamManagerService) CreateTeam(ctx context.Context) {
 
    // we are using the struct field variable
    code = m.RandomStringGenerator(5)
 
    // your application logic

    return  nil
}


在测试中,您可以使用自己的自定义函数,

myGenerator = func(length int) string {
        return "some string"
    }
    
   service := &teamManager.TeamManagerService{RandomStringGenerator: myGenerator}
   service.CreateTeam(context.Background())


你像我一样使用证明:D你可以做到这一点

// this is the mock version of the base36 file
package base36_mock

import "github.com/stretchr/testify/mock"

var Mock = mock.Mock{}

func GenerateRandomString(length int) string {
    args := Mock.Called(length)
    return args.String(0)
}


在测试中,您可以使用自己的自定义函数,

base36_mock.Mock.On("GenerateRandomString", 5).Return("my expmle code for this test").Once()
    
   service := &teamManager.TeamManagerService{RandomStringGenerator: base36_mock.GenerateRandomString}
   service.CreateTeam(context.Background())

y0u0uwnf

y0u0uwnf6#

警告:这可能会使可执行文件的大小增加一点,并消耗一点运行时性能。在我看来,如果golang有像宏或函数装饰器这样的功能,这会更好。
如果你想模拟函数而不改变它的API,最简单的方法是稍微改变一下实现:

func getPage(url string) string {
  if GetPageMock != nil {
    return GetPageMock()
  }

  // getPage real implementation goes here!
}

func downloader() {
  if GetPageMock != nil {
    return GetPageMock()
  }

  // getPage real implementation goes here!
}

var GetPageMock func(url string) string = nil
var DownloaderMock func() = nil

字符串
通过这种方式,我们实际上可以从其他函数中模拟出一个函数。为了更方便,我们可以提供这样的模拟样板:

// download.go
func getPage(url string) string {
  if m.GetPageMock != nil {
    return m.GetPageMock()
  }

  // getPage real implementation goes here!
}

func downloader() {
  if m.GetPageMock != nil {
    return m.GetPageMock()
  }

  // getPage real implementation goes here!
}

type MockHandler struct {
  GetPage func(url string) string
  Downloader func()
}

var m *MockHandler = new(MockHandler)

func Mock(handler *MockHandler) {
  m = handler
}


在测试文件中:

// download_test.go
func GetPageMock(url string) string {
  // ...
}

func TestDownloader(t *testing.T) {
  Mock(&MockHandler{
    GetPage: GetPageMock,
  })

  // Test implementation goes here!

  Mock(new(MockHandler)) // Reset mocked functions
}

bsxbgnwa

bsxbgnwa7#

我一直在类似的地方.我试图写unitTest的函数,有许多客户端调用它.让我提出2个选项,我探索.其中一个已经在这个线程中讨论,我将无论如何重复它的人搜索的缘故.

方法一:将需要mock的函数声明为全局变量

一种选择是声明全局变量(具有一些凹坑福尔斯)。
例如:

package abc

var getFunction func(s string) (string, error) := http.Get

func get_page(url string) string {
  ....
  resp, err := getFunction(url)
  ....
}

func downloader() {
  .....
}

字符串
测试函数如下:

package abc

func testFunction(t *testing.T) {
  actualFunction := getFunction
  getFunction := func(s string) (string, error) { 
     //mock implementation 
  }
  defer getFunction = actualFunction
  .....
  //your test
  ......
}

注意:测试和实际实现在同一个包中。

以上的方法思想是有限制的!
1.由于竞争条件的风险,不可能运行并行测试。
1.通过使函数成为一个变量,我们引入了一个小的风险,即引用被未来在同一个包中工作的开发人员修改。

方法二:创建 Package 函数

另一种方法是传递沿着你想模仿的方法作为函数的参数来实现可测试性。在我的例子中,我已经有很多客户端调用这个方法,因此,我想避免违反现有的合同。所以,我最终创建了一个 Package 函数。
例如:

package abc

type getOperation func(s string) (string, error)

func get_page(url string, op getOperation) string {
  ....
  resp, err := op(url)
  ....
}

//contains only 2 lines of code
func downloader(get httpGet) {
  op := http.Get
  content := wrappedDownloader(get, op)
}

//wraps all the logic that was initially in downloader()
func wrappedDownloader(get httpGet, op getOperation) {
  ....
  content := get_page(BASE_URL, op)
  ....
}


现在为了测试实际的逻辑,你将测试对wrappedDownloader而不是Downloader的调用,并且你将传递给它一个模拟的getOperation。这允许你测试所有的业务逻辑,同时不违反你与当前客户端的API合同。

brjng4g3

brjng4g38#

我是Golang的新手,但我花了几天时间试图找到一种方法来模拟第三方软件包(如http.Get)的功能,而不修改源代码。此时,我被迫得出结论,这在Golang中是不可能的,这是一个巨大的失望。

dxxyhpgq

dxxyhpgq9#

考虑到单元测试是这个问题的领域,强烈推荐你使用monkey。这个包可以让你在不改变原始源代码的情况下进行模拟测试。与其他答案相比,它更“非侵入性”。
联系我们

type AA struct {
 //...
}
func (a *AA) OriginalFunc() {
//...
}

字符串

模拟测试

var a *AA

func NewFunc(a *AA) {
 //...
}

monkey.PatchMethod(reflect.TypeOf(a), "OriginalFunc", NewFunc)


坏的一面是:

  • Dave.C提醒,此方法不安全。因此不要在单元测试之外使用它。
  • 是非惯用的Go。

好的一面是:

  • 是非侵入性的。让你在不改变主代码的情况下做事情。就像托马斯说的。
  • 让你用最少的代码改变包的行为(可能是第三方提供的)。

相关问题