6

I have unit tests for most of our code. But I cannot figure out how to generate unit tests coverage for certain code in main() in main package.

The main function is pretty simple. It is basically a select block. It reads flags, then either call another function/execute something, or simply print help on screen. However, if commandline options are not set correctly, it will exit with various error codes. Hence, the need for sub-process testing.

I tried sub-process testing technique but modified code so that it include flag for coverage:

cmd := exec.Command(os.Args[0], "-test.run=TestMain -test.coverprofile=/vagrant/ucover/coverage2.out")

Here is original code: https://talks.golang.org/2014/testing.slide#23 Explanation of above slide: http://youtu.be/ndmB0bj7eyw?t=47m16s

But it doesn't generate cover profile. I haven't been able to figure out why not. It does generate cover profile for main process executing tests, but any code executed in sub-process, of course, is not marked as executed.

I try to achieve as much code coverage as possible. I am not sure if I am missing something or if there is an easier way to do this. Or if it is just not possible.

Any help is appreciated.

Thanks

Amer

1
  • This is shadowed in search engine results by a not so relevant QA. Please consider amending the title to include unit test command line flags in Go. Commented Oct 30, 2020 at 10:25

3 Answers 3

6

I went with another approach which didn't involve refactoring main(): see this commit:

I use a global (unexported) variable:

var args []string

And then in main(), I use os.Args unless the private var args was set:

a := os.Args[1:]
if args != nil {
    a = args
}
flag.CommandLine.Parse(a)

In my test, I can set the parameters I want:

args = []string{"-v", "-audit", "_tests/p1/conf/gitolite.conf"}
main()

And I still achieve a 100% code coverage, even over main().

Sign up to request clarification or add additional context in comments.

3 Comments

You solved the next problem I was about to have. So far I was setting flags in unit tests using: flag.BoolVar(SHOWVERSION, "SHOWVERSION", true, "usage"). It works as long as I don't redefine to test flag with a different value. Thanks, I will be using your approach.
@Amer Yes, I always set flags in main(). I only set args in test.
Using this with Go 1.11, I found I had to reset flag.CommandLine on each test redefining the flags, to avoid a "flag redefined" panic.: lang-go for _, check := range checks { t.Run("flagging " + check.arg, func(t *testing.T) { flag.CommandLine = flag.NewFlagSet(cmd, flag.ContinueOnError) args = []string{check.arg} main() }) }
4

I would factor the logic that needs to be tested out of main():

func main() {
    start(os.Args)
}

func start(args []string) {
    // old main() logic
}

This way you can unit-test start() without mutating os.Args.

1 Comment

Thank you! I was so focused on making sub process testing work that didn't think about re-factoring. What we will do is move all os.Exit() calls in a wrapper function. That way we can just mock that function in tests.
1

Using @VonC solution with Go 1.11, I found I had to reset flag.CommandLine on each test redefining the flags, to avoid a "flag redefined" panic.:

    for _, check := range checks {
        t.Run("flagging " + check.arg, func(t *testing.T) {
            flag.CommandLine = flag.NewFlagSet(cmd, flag.ContinueOnError)
            args = []string{check.arg}
            main()
        })
    }

1 Comment

@VonC using NewFlagSet also enables you to use SetOutput, as in flag.CommandLine.SetOutput(someBuffer), in order to catch stderr output in your tests and check correctness for sad cases too.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.