提问者:小点点

Go中的模拟函数


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

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()

我发现了这个线程,它似乎是关于一个类似的问题。Julian Phillips提出了他的库Witmock作为解决方案,但我无法让它工作。老实说,这是我的测试代码的相关部分,对我来说主要是货物崇拜代码:

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':退出状态1输出:无法加载包:package_et/http:在
/var/folders/z9/ql_yn5h550s6shtb9c5sggj40000gn/t/withmock570825607/path/src/et/http中找到包http(chunked.go)和main(main_mock.go)

有模拟是我的测试问题的解决方案吗?我应该怎么做才能让它工作?


共3个答案

匿名用户

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

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)
}

如果不想将依赖项作为参数传递,还可以使 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()
}

匿名用户

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

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标准库中的这种模式将测试挂钩插入代码中,以使测试更容易:

  • https://golang.org/src/net/hook.go
  • https://golang.org/src/net/dial.go#L248
  • https://golang.org/src/net/dial_test.go#L701

匿名用户

我使用了一种稍微不同的方法,其中公共结构方法实现接口,但它们的逻辑仅限于包装将这些接口作为参数的私有(未导出)函数。这为您提供了模拟几乎所有依赖项所需的粒度,同时还可以从测试套件外部使用一个干净的API。

为了理解这一点,必须理解您可以访问您的测试用例中未导出的方法(即从您的< code>_test.go文件中),因此您测试这些方法,而不是测试除了包装之外没有内部逻辑的导出方法。

总结一下:测试未导出的函数,而不是测试导出的函数!

让我们举个例子。假设我们有一个Slack API结构,它有两个方法:

  • 向Slack网络钩子发送HTTP请求的SendMessage方法
  • 给定一个字符串切片的SendDataSynynsynally方法迭代它们并为每次迭代调用SendMessage

因此,为了测试<code>SendDataSynchronous</code>而无需每次发出HTTP请求,我们必须模拟<code>SendMessage</code〕,对吗?

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 要干净得多,并且要传递的参数更少,因为这里真正的依赖项只是自己实现所有这些接口的父接收器。然而,每个函数可能只依赖于其中的一部分(一个,也许是两个接口),这使得重构变得更加容易。很高兴通过查看函数签名来了解您的代码是如何真正耦合的,我认为它是一个强大的工具来防止代码的气味。

为了简单起见,我将所有内容放在一个文件中,以便您可以在这里运行代码,但我建议您也查看GitHub上的完整示例,这里是slack.go文件,这里是slack_test.go。

这是整件事。