简体   繁体   English

在Go中实现测试模拟的首选方式是什么?

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

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). 我在Go中构建了一个简单的CLI工具,用作各种密码存储(Chef Vault,Ansible Vault,Hashicorp Vault等)的包装。 This is partially as an exercise to get familiar with Go. 这部分是作为熟悉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. 在解决这个问题时,我遇到了编写测试的情况,发现我需要为很多事情创建interfaces ,只是为了能够模拟依赖关系。 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. 但是,我最近在阅读《 Go编程语言》,并找到了一个示例,其中他们以以下方式模拟了依赖关系。

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. 否则,我将为此创建一个interface (尽管当前只有一个实现)并将其注入Parse方法。 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? 什么时候更适合为模拟目的创建接口,而不是将具体函数存储在变量中以便在测试中重新声明?

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. 说了这么多,我发现interface方法比定义一个函数变量并为测试设置它更加通用和清晰。

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). 如果您需要模拟多个函数(在您的示例中,它只是一个函数),那么function variable方法将无法很好地扩展。

  • 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. 与使最终隐藏在实现中的功能变量相反,该interface使该功能/模块的行为更加清楚。

  • The interface allows you to inject a type with a state (a struct) which might be useful for configuring the behaviour of the mock. interface允许您注入带有状态(结构)的类型,这对于配置模拟的行为可能有用。

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". 对于简单的情况,您当然可以依靠“函数变量”方法,而对于更复杂的功能,可以使用“接口”,但是如果您希望保持一致并只使用一种方法,则可以使用“接口”。

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. 原因之一是AFAIK, 没有直接的方法来识别哪些结构实现了接口 ,如果我想知道模拟是否在做正确的事情,这对我来说很重要。

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. 在您的测试中,您肯定会希望在测试完成后使用deferstoreFunc重新分配回其原始的具体实现。

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. 顺便说一下, var storeFunc = func openStore() *Store不会编译。

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. 仅签名就清楚需要Store实现。 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. 但是,这很明显:解析是可以附加到商店的函数,它比解析周围的商店更有意义。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM