How to mock a package method in Go?

huangapple go评论77阅读模式
英文:

How to mock a package method in Go?

问题

假设我有一个包含以下代码的包:

package paths

type FilePath struct {
    PathA string
}

func (c FilePath) GetPathA() string {
    if err := PathExists(c.PathA); err != nil {
        return ""
    }
    return c.PathA + "foo"
}

func PathExists(p string) error {
    // 调用 os 和 file 方法
    return err
}

我该如何模拟 PathExists 的依赖以测试 FilePath?另外,方法 PathExists 也被许多其他包使用。(我愿意接受重构建议,以使其更易于测试,并考虑以下几点)

我已经了解了几种不同的方法,但似乎没有一种对我来说直观或符合惯例。

  1. 在包中有一个全局变量 PE := PathExists,在 GetPathA 中调用 err := PE(c.PathA),并在测试中用一个模拟方法覆盖 PE。

    问题:如果测试包是 paths_test,我将不得不导出 PE,这也允许包的客户端进行覆盖。

  2. 将 PathExists 设为 FilePath 的一个字段,并在测试中模拟该字段。

    问题:当客户端使用该包时,将不得不初始化 PathExists 字段,或者我提供一个形如 NewFilePath(PathA string) 的构造函数来为我初始化字段。在实际使用中有很多字段,因此这种方法也失败了。

  3. 使用接口并将其嵌入结构体中。当客户端使用时,使用实际方法进行初始化,用于测试时进行模拟。

    type PathExistser interface{ 
        PathExists(p string) error 
    }
    
    type FilePath struct{
        PathA string
        PathExister
    }
    
    type Actual struct{}
    
    func (a Actual) PathExists(p string) error {
       return PathExists(p)
    }
    

    问题:客户端还需要提供正确的接口实现。

我了解到了一些类似上述选项的方法,比如将方法 PathExists 作为 GetPathA 的参数等。所有这些方法都有相同的问题。基本上,我不希望这个包的用户必须弄清楚应该是什么样的输入参数才能确保结构体按预期工作。我也不希望用户覆盖 PathExists 的行为。

这似乎是一个非常直接的问题,我似乎在 Go 的测试或模拟方面漏掉了一些非常基本的东西。任何帮助将不胜感激,谢谢。

方法名仅为示例。实际上,GetPathA 或 PathExists 的实现可能更加复杂。

英文:

Let's say I have a package with the following code:

package paths

type FilePath struct {
    PathA string
}

func (c FilePath) GetPathA() string {
    if err := PathExists(PathA); err != nil {
    return ""
    }
    return PathA + "foo"
}

func PathExists(p string) error {
    // call os and file methods
    return err
}

How do I mock out the PathExists dependency to test FilePath? Also, method PathExists is being used by a lot of other packages as well. (I am open to suggestions of refactoring this to make it test friendly, keeping the following pointers in mind)

I have come across a few different approaches but none of them seems intuitive or idiomatic to me.

  1. Have a global variable PE := PathExists in the package; in GetPathA, call err := PE(PathA) and in the test overwrite PE with a mock method.

    Issue: If test package is something like paths_test, I will have to export PE which allows clients of the package to overwrite it as well.

  2. Make PathExists a field of FilePath and mock the field in test.

    Issue: Clients when using the package, will have to initialize PathExists field, or I provide a constructor of the form NewFilePath(PathtA string) which initializes the fields for me. In the actual use case there are a lot of fields, hence this approach fails as well.

  3. Use an interface and embed it within the struct. When client uses it initialize with the actual method and for test mock it.

    type PathExistser interface{ 
        PathExists(p string) error 
    }
    
    type FilePath struct{
        PathA string
        PathExister
    }
    
    type Actual struct{}
    
    func (a Actual) PathExists(p string) error {
       return PathExists(p)
    }
    

    Issue: Client again needs to provide the right implementation of the interface.

I have learnt of few more approaches doing something simimlar to the above options, such as make the method PathExists an argument for GetPathA, etc. All have the same concerns. Basically, I don't want the users of this package to have to figure out what should be the right input parameter to make sure the struct works as expected. Neither do I want the users to overwrite the behaviour PathExists.

This seems like a very straightforward problem and I seem to be missing something very funamental about go testing or mocking. Any help would be appreciated, thanks.

Method names are just for example. In reality GetPathA or PathExists would be way more complex.

答案1

得分: 3

要解决你的第一个方法中的问题,你可以使用一个内部包,然后在paths_test中导入该包,但是你的包的客户端将无法导入该包。

package paths

import (
    // ...
    "<your_module_path>/internal/osutil"
)

func PathExists(p string) error {
    return osutil.PathExists(p)
}
package osutil

var PathExists = func(p string) error {
    // 调用 os 和 file 方法
    return err
}

// 使用互斥锁确保如果你有多个使用 mockPathExists 并行运行的测试,
// 你可以避免数据竞争的可能性。
//
// 注意,如果你的所有测试都使用 MockPathExists,那么互斥锁才有用。
// 如果只有一些测试使用了 MockPathExists,而其他测试仍然直接或间接地
// 调用 paths.PathExists 函数,那么仍然可能遇到数据竞争问题。
var mu sync.Mutex

func MockPathExists(mock func(p string) error) (unmock func()) {
    mu.Lock()
    original := PathExists
    PathExists = mock
    return func() {
        PathExists = original
        mu.Unlock()
    }
}
package paths_test

import (
    // ...
    "<your_module_path>/internal/osutil"
)

func TestPathExists(t *testing.T) {
    unmock := osutil.MockPathExists(myPathExistsMockImpl)
    defer unmock()
    // 进行你的测试
}
英文:

To address the issue from your 1. approach, you can use an internal package which you'll then be able to import in paths_test but clients of your package won't be.

package paths

import (
    // ...
    &quot;&lt;your_module_path&gt;/internal/osutil&quot;
)

func PathExists(p string) error {
    return osutil.PathExists(p)
}
package osutil

var PathExists = func(p string) error {
    // call os and file methods
    return err
}

// Use a mutex to make sure that if you have
// multiple tests using mockPathExists and running
// in parallel you avoid the possiblity of a data race.
//
// NOTE that the mutex is only useful if *all* of your tests
// use MockPathExists. If only some do while others don&#39;t but
// still directly or indirectly cause the paths.PathExists
// function to be invoked then you still can run into a data
// race problem.
var mu sync.Mutex

func MockPathExists(mock func(p string) error) (unmock func()) {
    mu.Lock()
    original := PathExists
    PathExists = mock
    return func() {
        PathExists = original
        mu.Unlock()
    }
}
package paths_test

import (
    // ...
    &quot;&lt;your_module_path&gt;/internal/osutil&quot;
)

func TestPathExists(t *testing.T) {
    unmock := osutil.MockPathExists(myPathExistsMockImpl)
    defer unmock()
    // do your test
}

huangapple
  • 本文由 发表于 2021年7月23日 13:58:10
  • 转载请务必保留本文链接:https://go.coder-hub.com/68494658.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定