Go中的模拟函数

Gol*_*nks 129 unit-testing mocking go

我正在通过编写一个小型的个人项目来学习Go.虽然它很小,但我决定从一开始就进行严格的单元测试,以便在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])
    }
}
Run Code Online (Sandbox Code Playgroud)

我希望能够测试downloader()而不实际通过http获取页面 - 即通过模拟get_page(更容易,因为它只返回页面内容作为字符串)或http.Get().

我找到了这个帖子:https://groups.google.com/forum/#!topic/golang -nuts/6AN1E2CJOxI,这似乎是一个类似的问题.Julian Phillips介绍了他的库,Withmock(http://github.com/qur/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
}
Run Code Online (Sandbox Code Playgroud)

测试输出如下:

ERROR: Failed to install '_et/http': exit status 1
output:
can't load package: package _et/http: found packages http (chunked.go) and main (main_mock.go) in /var/folders/z9/ql_yn5h550s6shtb9c5sggj40000gn/T/withmock570825607/path/src/_et/http
Run Code Online (Sandbox Code Playgroud)

Withmock是我测试问题的解决方案吗?我该怎么办才能让它发挥作用?

web*_*rc2 171

感谢你练习好的测试!:)

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

方法1:get_page()作为参数传递downloader()

type PageGetter func(url string) string

func downloader(pageGetterFunc PageGetter) {
    // ...
    content := pageGetterFunc(BASE_URL)
    // ...
}
Run Code Online (Sandbox Code Playgroud)

主要:

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

func main() {
    downloader(get_page)
}
Run Code Online (Sandbox Code Playgroud)

测试:

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

func TestDownloader(t *testing.T) {
    downloader(mock_get_page)
}
Run Code Online (Sandbox Code Playgroud)

方法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)
    //...
}
Run Code Online (Sandbox Code Playgroud)

主要:

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

func main() {
    d := NewDownloader(get_page)
    d.download()
}
Run Code Online (Sandbox Code Playgroud)

测试:

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

func TestDownloader() {
    d := NewDownloader(mock_get_page)
    d.download()
}
Run Code Online (Sandbox Code Playgroud)

  • 我是唯一一个发现,为了测试我们必须改变主要代码/功能签名是可怕的吗? (119认同)
  • @Thomas我不确定你是否是唯一一个,但它实际上是测试驱动开发的根本原因 - 你的测试指导你编写生产代码的方式.可测试代码更加模块化.在这种情况下,Downloader对象的'get_page'行为现在是可插拔的 - 我们可以动态地改变它的实现.如果主要代码写得不好,您只需要更改主代码即可. (31认同)
  • @Thomas我不明白你的第二句话.TDD驱动更好的代码.您的代码更改是为了可测试(因为可测试代码必须是模块化的,具有深思熟虑的接口),但主要_purpose_是为了拥有更好的代码 - 拥有自动化测试只是一个非常好的第二个好处.如果你担心的是功能代码被改变只是为了在事后添加测试,我仍然会建议改变它只是因为很有可能有人有一天会想要阅读或修改它. (18认同)
  • @Thomas当然,如果你正在编写测试,那么你就不必处理这个难题了. (5认同)
  • 非常感谢!我和第二个一起去了.(还有一些我想要模拟的其他函数,因此将它们分配给结构更容易)顺便说一句.我喜欢Go.特别是它的并发功能很整齐! (4认同)
  • +1 的喜剧“_mocking in Go 没有它就很容易_”......与 Python 之类的东西相比,这相当困难。 (3认同)
  • @weberc2 我同意事先编写测试可以避免问题:) (2认同)
  • @Mihai 也许吧。我试图与原来的例子保持一致。不管怎样,这个意图对你来说更清楚,因为你正在借鉴 C# 的经验,虽然 C# 有闭包,但它的 Java 遗产引导其从业者思考对象和接口(并将闭包视为类似于“方便的语法糖”的东西) )。函数式程序员发现传递函数比接口更自然。Go 是惯用的多范式:例如,“net/http”定义了“Handler”(接口)和“HandlerFunc”(函数)。 (2认同)
  • 你如何实现 100% 的代码覆盖率?`func get_page(url string) string { /* ... */ }` 不可测试,因此无法实现完全覆盖 (2认同)
  • @JonathanHartley 虽然 Python 确实很容易进行模拟,但我认为 Python 的方法更糟糕,或者至少更难正确执行。我花了很多时间来追踪问题,其中模拟的行为就像方法存在一样,而实际上它们并不存在,从而导致与实际问题相去甚远的失败,甚至更糟:误报。除此之外,像“patch”这样的东西会鼓励不可维护的代码(在其他语言中是“不可测试的”代码)。同样,由于模拟非常简单,因此严重的模拟很常见,这使得这些测试几乎毫无用处。这只是我作为专业 Python 开发人员的 0.02 美元。 (2认同)
  • 好吧,听起来我们大多同意。DI FTW。但是在很多情况下这是不可能的 - 最突出的是,当在一个没有以这种方式工作的团队中工作时。对于这样的情况,以及一堆其他边缘情况,你真的需要“补丁”。 (2认同)

Jak*_*ake 17

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

var get_page = func(url string) string {
    ...
}
Run Code Online (Sandbox Code Playgroud)

您可以在测试中覆盖它:

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

但是,如果他们测试您覆盖的功能的功能,那么您的其他测试可能会失败!

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

  • 如果您愿意,可以使用Downvote,这是小包装的可接受模式,以避免与DI相关的样板.包含该函数的变量仅对包的范围"全局",因为它未被导出.这是一个有效的选择,我提到了缺点,选择自己的冒险. (6认同)
  • “但要小心,如果其他测试测试您覆盖的函数的功能,则它们可能会失败!” 可能会在覆盖之前存储原始功能并在测试后恢复它会有所帮助。`get_page_orig :=.get_page /* 覆盖 get_page 并测试 */ get_page =get_page_orig` (4认同)
  • 需要注意的一点是,以这种方式定义的函数_cannot_是递归的. (3认同)
  • 我同意@Jake的说法,这种方法有其自己的地位. (2认同)

Fra*_*ula 10

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

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

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

让我们举个例子.假设我们有一个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()
}
Run Code Online (Sandbox Code Playgroud)

我喜欢这种方法的方法是,通过查看未导出的方法,您可以清楚地看到依赖关系是什么.与此同时,您导出的API更清晰,传递的参数更少,因为这里的真正依赖只是实现所有这些接口本身的父接收器.然而,每个函数都可能只依赖于它的一部分(一个,可能是两个接口),这使得重构变得更容易.很高兴看到你的代码是如何通过查看函数签名真正耦合的,我认为它是一个强大的工具来防止嗅到代码.

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

在这里,整个事情:)


Fal*_*len 7

我会做点什么,

主要

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])
    }
}
Run Code Online (Sandbox Code Playgroud)

测试

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 (....
Run Code Online (Sandbox Code Playgroud)

我会避免_在golang.更好地使用camelCase

  • @Fallen,这几乎就是我一年多后写的答案。 (2认同)