Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mocking os.GetEnv("ENV_VAR")

I am trying to mock the Go function os.GetEnv() in my test files so that I can get the desired value for a particular environment variable.

For example I have defined.

abc := os.GetEnv("XYZ_URL")

Here I should be able to get the needed value for the variable abc. Also I have several places with the GetEnv functions.

It will be really helpful if someone can give me a workaround without the help of any Go framework.

like image 203
Jaward Sally Avatar asked Feb 05 '21 10:02

Jaward Sally


1 Answers

First, you can't mock that function. You can only mock something exposed as an interface.

Second, you probably don't need to. Mocks, broadly speaking, are over-used, and should be avoided whenever possible.

When testing environment variables, you have few options.

If you're using Go 1.17 or newer, you can take advantage of the new Setenv function, which sets the environment variable for the duration of the current test only:

func TestFoo(t *testing.T) {
    t.Setenv("XYZ_URL", "http://example.com")
    /* do your tests here */
}

For older versions of Go, consider these options:

  1. Create an object which can be mocked/doubled, which exposes the necessary functionality. Example:
type OS interface {
    Getenv(string) string
}

type defaultOS struct{}

func (defaultOS) Getenv(key string) string {
    return os.Getenv(key)
}

// Then in your code, replace `os.Getenv()` with:

myos := defaultOS{}
value := myos.Getenv("XYZ_URL")

And in your tests, create a custom implementation that satisfies the interface, but provides the values you need for testing.

This approach is useful for some things (like wrapping the time package), but is probably a bad approach for os.Getenv.

  1. Make your functions not depend on os.Getenv, and instead just pass the value in. Example, instead of:
func connect() (*DB, error) {
    db, err := sql.Connect(os.Getenv("XYZ_URL"), ...)
    /* ... */
    return db, err
}

use:

func connect(url string) (*DB, error) {
    db, err := sql.Connect(url, ...)
    /* ... */
    return db, err
}

In a sense, this only "moves" the problem--you may still want to test the caller, which uses os.Getenv(), but you can at least reduce the surface area of your API that depends on this method, which makes the third approach easier.

  1. Set the environment expliclitly during your tests. Example:
func TestFoo(t *testing.T) {
    orig := os.Getenv("XYZ_URL")
    os.Setenv("XYZ_URL", "http://example.com")
    t.Cleanup(func() { os.Setenv("XYZ_URL", orig) })
    /* do your tests here */
}

This approach does have limitations. In particular, it won't work to run multiple of these tests in parallel, so you still want to minimize the number of these tests you run.

This means that approaches 2 and 3 in conjunction with each other can be very powerful.

  1. A final option is to create a function variable, that can be replaced in tests. I've talked about this in another post, but for your example it could look like this:
var getenv = os.Getenv

/* ... then in your code ... */

func foo() {
    value := getenv("XYZ_URL") // Instead of calling os.Getenv directly
}

and in a test:

func TestFoo(t *testing.T) {
    getenv = func(string) string { return "http://example.com/" }
    /* ... your actual tests ... */
}

This has many of the same limitations as option #3, in that you cannot run multiple tests in parallel, as they will conflict.

like image 187
Flimzy Avatar answered Sep 30 '22 20:09

Flimzy