【问题标题】:How to mock a package method in Go?如何在 Go 中模拟一个包方法?
【发布时间】:2021-09-30 07:32:44
【问题描述】:

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

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
}

如何模拟 PathExists 依赖项来测试 FilePath?此外,许多其他软件包也正在使用方法PathExists。 (我愿意接受重构它以使其测试友好的建议,请牢记以下几点)

我遇到了几种不同的方法,但对我来说,它们都不直观或惯用。

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

    问题:如果测试包类似于paths_test,我将不得不导出PE,这样包的客户端也可以覆盖它。

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

    问题:客户在使用包时,必须初始化PathExists 字段,或者我提供NewFilePath(PathtA 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 测试或模拟的非常重要的东西。任何帮助将不胜感激,谢谢。

方法名称只是举例。实际上GetPathAPathExists 会更复杂。

【问题讨论】:

    标签: unit-testing go testing mocking


    【解决方案1】:

    要解决您的1. 方法中的问题,您可以使用内部包,然后您可以将其导入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 {
        // 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
    }
    

    【讨论】:

    • internal package-level var 是个好技巧!
    • 啊确实,这是一个不错的技巧!但是,为具有此包的同一库/应用程序编写代码的开发人员仍然可能通过覆盖 osutil.PathExists 意外引入错误(我知道我非常挑剔,但只是想确保我完全了解所有解决方案的优缺点)
    • 如果您可以参考任何使用这种方法或有类似问题的标准库,这将有助于巩固我对这种方法的信心
    • @kernel0707 我不知道标准库是否使用这种方法。如果您想遵循标准库中使用的测试模式,那么您可能不得不完全放弃模拟的想法,因为我认为这些使用不多。相反,至少在net 包中,很多功能都是使用“测试挂钩”进行测试的。关于开发人员引入的错误的另一点,为了避免这些错误,您应该编写一个可以捕获此类错误的测试,或者如果这是不可能的,那么您在合并拉取请求之前进行代码审查。
    • @kernel0707 请记住,标准库本身确实具有导出的变量,如果它们被某些客户端代码不当更改,可能会造成严重破坏。在我看来,开发人员的工作是充分了解它们的依赖关系,以免滥用它们并自取其辱。在 Go 中实现你想要的东西是困难和丑陋的,如果不是完全不可能的话,应该被认为是你需要放松你的要求或切换到提供你正在寻找的安全机制的语言的一个指标为。
    猜你喜欢
    • 1970-01-01
    • 2019-02-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-09-09
    • 1970-01-01
    相关资源
    最近更新 更多