简体   繁体   中英

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). 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?

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".

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.

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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