What is the preferred way to implement testing mocks in Go?

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

What is the preferred way to implement testing mocks in Go?

问题

我正在构建一个简单的Go命令行工具,作为各种密码存储(Chef Vault、Ansible Vault、Hashicorp Vault等)的包装器。这部分是为了熟悉Go而进行的练习。

在开发过程中,我遇到了一个情况,即在编写测试时,我发现我需要为许多东西创建接口,只是为了能够模拟依赖关系。因此,一个相当简单的实现似乎有很多抽象,为了测试而抽象。

然而,我最近在阅读《Go编程语言》时发现了一个示例,他们在其中以以下方式模拟了他们的依赖关系。

func Parse() map[string]string {

    s := openStore()

    // 使用s进行解析成map的操作...

    return s.contents
}

var storeFunc = func openStore() *Store {
    // 打开存储的具体实现
}


// 在测试文件中...

func TestParse(t *testing.T) {
    openStore := func() {
        // 设置模拟的内容...
    } 

    parse()

    // 等等...
}

因此,为了进行测试,我们将这个具体实现存储在一个变量中,然后在测试中可以重新声明该变量,并返回我们需要的内容。

否则,我将为此创建一个接口(尽管目前只有一个实现),并将其注入到Parse方法中。这样,我们就可以在测试中模拟它。

所以我的问题是:每种方法的优缺点是什么?在何时更适合为了模拟而创建一个接口,而不是将具体函数存储在变量中以便在测试中重新声明?

英文:

I am building a simple CLI tool in in Go that acts as a wrapper for various password stores (Chef Vault, Ansible Vault, Hashicorp Vault, etc). This is partially as an exercise to get familiar with Go.

In working on this, I came across a situation where I was writing tests and found I needed to create interfaces for many things, just to have the ability to mock dependencies. As such, a fairly simple implementation seems to have a bunch of abstraction, for the sake of the tests.

However, I was recently reading The Go Programming Language and found an example where they mocked their dependencies in the following way.

func Parse() map[string]string {

	s := openStore()
	
	// Do something with s to parse into a map…

	return s.contents
}

var storeFunc = func openStore() *Store {
	// concrete implementation for opening store
}


// and in the testing file…


func TestParse(t *testing.T) {
    openStore := func() {
        // set contents of mock…
    } 

    parse()

    // etc...

}

So for the sake of testing, we store this concrete implementation in a variable, and then we can essentially re-declare the variable in the tests and have it return what we need.

Otherwise, I would have created an interface for this (despite currently only having one implementation) and inject that into the Parse method. This way, we could mock it for the test.

So my question is: What are the advantages and disadvantages of each approach? When is it more appropriate to create an interface for the purposes of a mock, versus storing the concrete function in a variable for re-declaration in the test?

答案1

得分: 1

没有一种“正确的方法”来回答这个问题。

话虽如此,我发现“接口(interface)”的方法比定义一个函数变量并为测试设置它更通用和更清晰。

以下是一些关于这个问题的评论:

  • 如果你需要模拟多个函数(在你的例子中只有一个函数),那么“函数变量”方法的可扩展性就不太好。

  • “接口”更清楚地表明了注入到函数/模块中的行为,而函数变量则隐藏在实现中。

  • “接口”允许你注入一个带有状态(结构体)的类型,这对于配置模拟行为可能是有用的。

当然,对于简单的情况,你可以依赖“函数变量”方法,并对于更复杂的功能使用“接口”,但如果你想保持一致并只使用一种方法,我会选择“接口”。

英文:

There is no "right way" of answering this.

Having said this, I find the interface approach more general and more clear than defining a function variable and setting it for the test.

Here are some comments on why:

  • The function variable approach does not scale well if there are several functions you need to mock (in your example it is just one function).

  • The interface makes more clear which is the behaviour being injected to the function/module as opposed to the function variable which ends up hidden in the implementation.

  • The interface allows you to inject a type with a state (a struct) which might be useful for configuring the behaviour of the mock.

You can of course rely on the "function variable" approach for simple cases and use the "interface" for more complex functionality, but if you want to be consistent and use just one approach I'd go with the "interface".

答案2

得分: 1

为了测试目的,我倾向于使用你描述的模拟方法,而不是创建新的接口。其中一个原因是,据我所知,没有直接的方法来识别哪些结构体实现了一个接口,这对我来说很重要,因为我想知道模拟是否做得正确。

这种方法的主要缺点是变量本质上是一个包级别的全局变量(即使它是未导出的)。因此,声明全局变量的所有缺点都适用。

在你的测试中,你肯定会想要使用deferstoreFunc重新赋值为其原始的具体实现,一旦测试完成。

var storeFunc = func openStore() *Store {
    // 打开存储的具体实现
}

// 在测试文件中...
func TestParse(t *testing.T) {
    storeFuncOriginal := storeFunc
    defer func() {
        storeFunc = storeFuncOriginal
    }()

    storeFunc := func() {
        // 设置模拟的内容...
    } 

    parse()

    // 等等...
}

顺便说一下,var storeFunc = func openStore() *Store是无法编译通过的。

英文:

For testing purposes, I tend to use the mocking approach you described instead of creating new interfaces. One of the reasons being, AFAIK, there are no direct ways to identify which structs implement an interface, which is important to me if I wanted to know whether the mocks are doing the right thing.

The main drawback of this approach is that the variable is essentially a package-level global variable (even though it's unexported). So all the drawbacks with declaring global variables apply.

In your tests, you will definitely want to use defer to re-assign storeFunc back to its original concrete implementation once the tests completed.

var storeFunc = func *Store {
    // concrete implementation for opening store
}

// and in the testing file…
func TestParse(t *testing.T) {
    storeFuncOriginal := storeFunc
    defer func() {
        storeFunc = storeFuncOriginal
    }()

    storeFunc := func() {
        // set contents of mock…
    } 

    parse()

    // etc...
}

By the way, var storeFunc = func openStore() *Store won't compile.

答案3

得分: 0

我以不同的方式解决这个问题。给定以下代码:

function Parse(s Store) map[string] string{
  // 在接口 Store 上执行一些操作
}

你有几个优势:

  1. 你可以根据需要使用模拟或存根 Store。
  2. 在我看来,代码更加透明。仅凭函数签名就清楚地表明需要一个 Store 实现。而且代码不需要被打开 Store 的错误处理所污染。
  3. 代码文档可以保持更简洁。

然而,这也表明了一个很明显的事实:Parse 是一个可以附加到存储的函数,这很可能比在周围解析存储更有意义。

英文:

I tackle the problem differently. Given

function Parse(s Store) map[string] string{
  // Do stuff on the interface Store
}

you have several advantages:

  1. You can use a mock or a stub Store as you see fit.
  2. Imho, the code becomes more transparent. The signature alone makes clear that a Store implementation is required. And the code does not need to be polluted with error handling for opening the Store.
  3. The code documentation can be kept more concise.

However, this makes something pretty obvious: Parse is a function which can be attached to a store, which most likely makes more sense than to parse the store around.

huangapple
  • 本文由 发表于 2017年8月9日 02:49:54
  • 转载请务必保留本文链接:https://go.coder-hub.com/45575942.html
匿名

发表评论

匿名网友

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

确定