Photo by: Gigi (original)

Using Go Flags in Tests

Many developers are familiar with the flag package from the Go standard library. This package provides an easy method for adding command line options and argument parsing for Go programs. What may not be as well known is that flag can be used in Go unit tests as well.

The go test Command

To understand how flag might be used in go tests, first we need to understand how the go test command works. When you run go test it compiles all the files ending in _test.go into a test binary and then runs that binary to execute the tests. Since the go test binary is simply a compiled go program, it can process command line arguments like any other program – although with some caveats, as we will see.

Parsing Flags

The standard way of using the flag package in a program is to define one or more flag variables and the call the flag.Parse() command in the main() function of the program.

For example:

package main

import (
    "flag"
    "fmt"
)

func Greet(loc string) string {
    return fmt.Sprintf("Hello, %s!", loc)
}

func main() {

    var loc string

    flag.StringVar(&loc, "location", "World", "Name of location to greet")
    flag.Parse()

    fmt.Println(Greet(loc))
}

Which produces the following output, depending on the command line arguments passed:

$ go run hello.go
Hello, World!

$ go run hello.go -location 'San Francisco'
Hello, San Francisco!

There is nothing explicitly requiring us to call flag.Parse() from main(), we simply need a place to call Parse() once (and only once) before any of the flag variables are referenced. main() (or a function called by main() early in the execution phase) is an easy choice to achieve this.

Parsing Test Flags

The above approach does not directly work for Go tests, as we typically don't write main() functions for unit tests, we simply write test functions and let the tooling handle things from there. Where do we call flag.Parse from, then?

Well it turns out, the test binary generated by go test is already using the flag package internally and calls flag.Parse() during normal operation. All we need to do is to define our flag variables as globals so they are known before running flag.Parse().

The easiest way is to define our global variable and immediately set it to the return value from flag.String, flag.Boolean or the like. For example:

package main

import (
    "flag"
    "testing"
)

var loc = flag.String("location", "World", "Name of location to greet")

func TestGreet(t *testing.T) {

    res := Greet(*loc)

    if res != "Hello, San Francisco!" {
        t.Errorf("String mismatch on test")
    }
}

Notice that loc is now a *string not a string in the previous example, so we need to use the dereferenced form, *loc, when calling Greet().

Now when we run our tests, we can see that the test will only pass when “San Francisco” is passed as an argument:

$ go test
--- FAIL: TestGreet (0.00s)
    hello_test.go:20: String mismatch on test
FAIL
exit status 1
FAIL    _/home/jbowen   0.001s

$ go test -location 'Los Angeles'
--- FAIL: TestGreet (0.00s)
    hello_test.go:20: String mismatch on test
FAIL
exit status 1
FAIL    _/home/jbowen   0.001s

$ go test -location 'San Francisco'
PASS
ok      _/home/jbowen   0.001s

OK .. But Why?

Using a command line flag to determine whether a test will pass isn't a great use case, but there are other use cases for flags that make this approach worth considering:

Selectively Running / Not Running Certain Tests

One use case I have used test flags for is to only run a test (or tests) if a boolean flag is set. This comes in handy when writing code to access a network based API. By defining a flag, offline, I can precede any tests requiring network access with a simple test:

package mypkg

import (
    "testing"
)

var offline = flag.Bool("offline", false, "only perform local tests")

func TestSomethingRemote(t *testing.T) {

    if *offline {
        t.Skip("skipping test in offline mode")
    }

    ...
}

With the above code, TestSomethingRemote() will only be run if *offline is false. By passing -offline to go test we can automatically skip this test … perfect for doing quick tests when you might not have online access, such as when flying.

Note that go test has a built-in form of this filter with the -short option. Tests functions can query testing.Short() to see if the option is set and decide whether to run or return via Skip().

Passing In Secrets / Configuration

Some tests (again often when working with remote resources) may require values that should not be shared or stored in code repositories (e.g. API keys, passwords, usernames). One possible approach to providing these to unit tests without saving them to the git repository would be to pass them in as flag arguments (or, if there are several, pass in the name of a config file to be parsed for the values).

Generating ‘Golden’ Results

Mitchell Hashimoto (founder of HashiCorp and creator of tools such as Vagrant), outlined a use case for using test flags to save ‘golden’ results when testing against a remote resource in his presentation, Advanced Testing With Go.

Golden results are a way of easily testing more complex output or state in some code by saving output from known good unit tests to a file and then using this output to compare against in future tests. This is especially useful when format or content might change in the future. Mitchell's example shows how an -update flag can be used to trigger the go test program to generate new golden files rather than perform tests. In this way, the same code is used for generating and comparing the golden files.

Caveats

Piggybacking on the built-in behavior of go test does present a few issues that one needs to be aware of:

Overlapping Flag Names

The go test command has a number of flag options that it parses for its own use. If you define a flag using the same name as a built-in option, it will not be seen by the test binary itself as the go tooling will consume before executing the generated test binary.

For instance, if we were to define a boolean flag named short and try to access in our program it will not work because -short is flag for go test.

package main

import (
    "flag"
    "testing"
)

var short = flag.Bool("short", false, "short")

func TestShort(t *testing.T) {
    if !*short {
        t.Fail()
    }
}

If we call go test with the -short option, our test function still sees the short variable as false, because the test binary's flag.Parse() never sees the option.

$ go test -short
--- FAIL: TestShort (0.00s)
FAIL

We can prove this is due to go test by compiling (-c) and saving the test binary as a file named “test” (-o test) and then calling it directly:

$ go test -c -o test
$ ./test
--- FAIL: TestShort (0.00s)
FAIL
$ ./test -short
PASS

One workaround for this issue is to use the -args option to go test to indicate all the remaining arguments are meant to be passed directly to the test binary:

$ go test -args -short
PASS

However, it is likely better to rename your arguments to not conflict with existing flags.

Positional Arguments

By default, tests from go test cannot parse positional arguments because, once again, go test intercepts them for its own use (as list of packages to test). As with the case above this can be remedied by using the -args option:

go test -args arg1 arg2

Wrapping Up

Hopefully this post has proven useful in understanding the hows and whys of using flags in Go tests. I plan to write more Go focused articles in the near future, so please stay tuned!