英文:
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 也被许多其他包使用。(我愿意接受重构建议,以使其更易于测试,并考虑以下几点)
我已经了解了几种不同的方法,但似乎没有一种对我来说直观或符合惯例。
-
在包中有一个全局变量 PE := PathExists,在 GetPathA 中调用 err := PE(c.PathA),并在测试中用一个模拟方法覆盖 PE。
问题:如果测试包是 paths_test,我将不得不导出 PE,这也允许包的客户端进行覆盖。
-
将 PathExists 设为 FilePath 的一个字段,并在测试中模拟该字段。
问题:当客户端使用该包时,将不得不初始化 PathExists 字段,或者我提供一个形如 NewFilePath(PathA string) 的构造函数来为我初始化字段。在实际使用中有很多字段,因此这种方法也失败了。
-
使用接口并将其嵌入结构体中。当客户端使用时,使用实际方法进行初始化,用于测试时进行模拟。
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.
-
Have a global variable
PE := PathExists
in the package; inGetPathA
, callerr := PE(PathA)
and in the test overwritePE
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. -
Make
PathExists
a field ofFilePath
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 formNewFilePath(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. -
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 (
// ...
"<your_module_path>/internal/osutil"
)
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'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 (
// ...
"<your_module_path>/internal/osutil"
)
func TestPathExists(t *testing.T) {
unmock := osutil.MockPathExists(myPathExistsMockImpl)
defer unmock()
// do your test
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论