【问题标题】:Mock functions in GoGo 中的模拟函数
【发布时间】:2013-10-10 16:04:15
【问题描述】:

我对依赖关系感到困惑。我希望能够用模拟函数调用替换一些函数调用。这是我的代码的 sn-p:

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

我发现this thread 似乎是一个类似的问题。 Julian Phillips 提出了他的库 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' 失败:退出状态 1 输出:无法加载 package: package _et/http: 找到包 http (chunked.go) 和 main (main_mock.go) 在
/var/folders/z9/ql_yn5h550s6shtb9c5sggj40000gn/T/withmock570825607/path/src/_et/http

Withmock 能否解决我的测试问题?我应该怎么做才能让它工作?

【问题讨论】:

  • 既然您正在深入研究 Go 单元测试,请查看 GoConvey 以获得进行行为驱动测试的好方法...和预告片:自动更新Web UI 即将推出,它也适用于原生“go test”测试。

标签: unit-testing mocking go


【解决方案1】:

就我个人而言,我不使用gomock(或任何与此相关的模拟框架;没有它,Go 中的模拟非常容易)。我要么将依赖项作为参数传递给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()
}

【讨论】:

  • 非常感谢!我和第二个一起去了。 (我还想模拟一些其他功能,因此将它们分配给结构更容易)顺便说一句。我有点喜欢围棋。尤其是它的并发特性很简洁!
  • 只有我一个人发现为了测试我们必须更改主要代码/函数签名很糟糕吗?
  • @Thomas 我不确定你是不是唯一一个,但这实际上是测试驱动开发的根本原因——你的测试指导你编写生产代码的方式。可测试的代码更加模块化。在这种情况下,Downloader 对象的“get_page”行为现在是可插入的——我们可以动态地改变它的实现。如果一开始就写得不好,你只需要改变你的主要代码。
  • @Thomas 我不明白你的第二句话。 TDD 驱动更好的代码。你的代码为了可测试而改变(因为可测试的代码必须是模块化的,带有经过深思熟虑的接口),但主要的目的是有更好的代码——拥有自动化测试只是一个很棒的次要益处。如果您担心更改功能代码只是为了在事后添加测试,我仍然建议您更改它,因为很可能有一天有人会想要阅读或更改该代码。
  • @Thomas 当然,如果你边写边写测试,你就不必处理这个难题。
【解决方案2】:

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

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

【讨论】:

  • 如果你愿意,可以投反对票,对于小型包来说,这是一种可以接受的模式,以避免与 DI 相关联的样板。包含函数的变量仅对包的范围是“全局的”,因为它没有被导出。这是一个有效的选择,我提到了缺点,选择你自己的冒险。
  • 需要注意的是,以这种方式定义的函数不能是递归的。
  • 我同意@Jake 的观点,即这种方法有它的位置。
  • "但请注意,如果您的其他测试测试您覆盖的函数的功能,它们可能会失败!"可能在覆盖之前存储原始功能并在测试后恢复它会有所帮助。 get_page_orig :=.get_page /* override get_page and test */ get_page =get_page_orig
  • @BenSandler 迟到了,但从技术上讲,你可以让这个递归:var get_page func(); func init() { get_page = func() { get_page() } }
【解决方案3】:

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

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

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

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

  • SendMessage 方法将 HTTP 请求发送到 Slack webhook
  • 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 文件,这里是 @987654323 @。

还有here 整件事。

【讨论】:

  • 这实际上是一种有趣的方法,并且有关在测试文件中访问私有方法的花絮非常有用。它让我想起了 C++ 中的 pimpl 技术。但是,我认为应该说测试私有函数是危险的。私有成员通常被认为是实现细节,并且比公共接口更有可能随着时间而改变。不过,只要您只测试公共接口周围的私有包装器,就可以了。
  • 是的,一般来说我同意你的看法。在这种情况下,尽管私有方法主体与公共方法主体完全相同,但您将测试完全相同的东西。两者之间的唯一区别是函数参数。这就是允许您根据需要注入任何依赖项(无论是否模拟)的技巧。
  • 是的,我同意。我只是说,只要您将其限制为包装这些公共方法的私有方法,您就可以开始使用了。只是不要开始测试作为实现细节的私有方法。
  • 值得注意的是,这种方法严重影响了代码覆盖率工具。由于未经测试的公共 API,使用 go test -cover 等工具的覆盖率会下降很多。
【解决方案4】:

我会做类似的事情,

主要

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 中使用 _Better use camelCase

【讨论】:

  • 是否有可能开发一个可以为您执行此操作的软件包。我在想类似:p := patch(mockGetPage, getPage); defer p.done()。我是新手,正在尝试使用unsafe 库来做到这一点,但在一般情况下似乎不可能做到。
  • @Fallen 这几乎是我在一年多之后写的答案。
  • 1.唯一的相似之处是全局 var 方式。 @Jake 2。简单胜于复杂。 weberc2
  • @fallen 我不认为你的例子更简单。传递参数并不比改变全局状态更复杂,但是依赖全局状态会引入很多其他方式不存在的问题。例如,如果你想并行化你的测试,你将不得不处理竞争条件。
  • 几乎一样,但不是:)。在这个答案中,我看到了如何将函数分配给 var 以及如何允许我为测试分配不同的实现。我无法更改我正在测试的函数的参数,所以这对我来说是一个不错的解决方案。另一种方法是使用带有模拟结构的接收器,我还不知道哪个更简单。
【解决方案5】:

警告:这可能会稍微增加可执行文件的大小并降低运行时性能。 IMO,如果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
}

【讨论】:

    【解决方案6】:

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

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

    你正在像我一样使用 testify :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())
    

    【讨论】:

      【解决方案7】:

      考虑到单元测试是这个问题的领域,强烈推荐你使用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。

      好的一面是:

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

      【讨论】:

      • 请不要这样做。它是完全不安全的,并且可以破坏各种 Go 内部结构。更不用说它甚至不是远程惯用的 Go。
      • @DaveC 我尊重您对 Golang 的体验,但怀疑您的意见。 1.安全并不意味着软件开发的全部,做功能丰富和方便的事情。 2. 惯用的 Golang 不是 Golang,是它的一部分。如果一个项目是开源的,那么其他人在它上面玩脏是很常见的。社区至少应该鼓励它而不是压制它。
      • 该语言称为 Go。不安全是指它会破坏 Go 运行时,比如垃圾收集。
      • 对我来说,不安全对于单元测试来说很酷。如果每次进行单元测试时都需要使用更多“接口”重构代码。使用不安全的方法来解决它更适合我。
      • @DaveC 我完全同意这是一个非常糟糕的想法(我的答案是投票率最高且被接受的答案),但我认为这不会破坏 GC,因为 Go GC 是保守的并打算处理这样的案件。不过,我很乐意得到纠正。
      猜你喜欢
      • 1970-01-01
      • 2015-05-28
      • 2022-10-15
      • 2016-05-08
      • 2021-12-24
      • 1970-01-01
      • 2021-08-13
      • 2015-08-17
      • 2019-02-18
      相关资源
      最近更新 更多