How to test the main package functions in golang?

49,942

Solution 1

when you specify files on the command line, you have to specify all of them

Here's my run:

$ ls
main.go     main_test.go
$ go test *.go
ok      command-line-arguments  0.003s

note, in my version, I ran with both main.go and main_test.go on the command line

Also, your _test file is not quite right, you need your test function to be called TestXXX and take a pointer to testing.T

Here's the modified verison:

package main

import (
    "testing"
)

func TestFoo(t *testing.T) {
    t.Error(foo())
}

and the modified output:

$ go test *.go
--- FAIL: TestFoo (0.00s)
    main_test.go:8: Foo
FAIL
FAIL    command-line-arguments  0.003s

Solution 2

Unit tests only go so far. At some point you have to actually run the program. Then you test that it works with real input, from real sources, producing real output to real destinations. For real.

If you want to unit test a thing move it out of main().

Solution 3

This is not a direct answer to the OP's question and I'm in general agreement with prior answers and comments urging that main should be mostly a caller of packaged functions. That being said, here's an approach I'm finding useful for testing executables. It makes use of log.Fataln and exec.Command.

  1. Write main.go with a deferred function that calls log.Fatalln() to write a message to stderr before returning.
  2. In main_test.go, use exec.Command(...) and cmd.CombinedOutput() to run your program with arguments chosen to test for some expected outcome.

For example:

func main() {
    // Ensure we exit with an error code and log message
    // when needed after deferred cleanups have run.
    // Credit: https://medium.com/@matryer/golang-advent-calendar-day-three-fatally-exiting-a-command-line-tool-with-grace-874befeb64a4
    var err error
    defer func() {
        if err != nil {
            log.Fatalln(err)
        }
    }()

    // Initialize and do stuff

    // check for errors in the usual way
    err = somefunc()
    if err != nil {
        err = fmt.Errorf("somefunc failed : %v", err)
        return
    }

    // do more stuff ...

 }

In main_test.go,a test for, say, bad arguments that should cause somefunc to fail could look like:

func TestBadArgs(t *testing.T) {
    var err error
    cmd := exec.Command(yourprogname, "some", "bad", "args")
    out, err := cmd.CombinedOutput()
    sout := string(out) // because out is []byte
    if err != nil && !strings.Contains(sout, "somefunc failed") {
        fmt.Println(sout) // so we can see the full output 
        t.Errorf("%v", err)
    }
}

Note that err from CombinedOutput() is the non-zero exit code from log.Fatalln's under-the-hood call to os.Exit(1). That's why we need to use out to extract the error message from somefunc.

The exec package also provides cmd.Run and cmd.Output. These may be more appropriate than cmd.CombinedOutput for some tests. I also find it useful to have a TestMain(m *testing.M) function that does setup and cleanup before and after running the tests.

func TestMain(m *testing.M) {
    // call flag.Parse() here if TestMain uses flags
    os.Mkdir("test", 0777) // set up a temporary dir for generate files

    // Create whatever testfiles are needed in test/

    // Run all tests and clean up
    exitcode := m.Run()
    os.RemoveAll("test") // remove the directory and its contents.
    os.Exit(exitcode)

Solution 4

How to test main with flags and assert the exit codes

@MikeElis's answer got me half way there, but there was a major part missing which Go's own flag_test.go help me figure out.

Disclaimer

You essentially want to run your app and test correctness. So please label this test anyway you want and file it in that category. But its worth trying this type of test out and seeing the benefits. Especially if your a writing a CLI app.

The idea is to run go test as usual, and

  1. Have a unit test run "itself" in a sub-process using the test build of the app that go test makes (see line 86)
  2. We also pass environment variables (see line 88) to the sub-process that will execute the section of code that will run main and cause the test to exit with main's exit code:
    if os.Getenv(SubCmdFlags) != "" {
        // We're in the test binary, so test flags are set, lets reset it so
        // so that only the program is set
        // and whatever flags we want.
        args := strings.Split(os.Getenv(SubCmdFlags), " ")
        os.Args = append([]string{os.Args[0]}, args...)
    
        // Anything you print here will be passed back to the cmd.Stderr and
        // cmd.Stdout below, for example:
        fmt.Printf("os args = %v\n", os.Args)
    
        // Strange, I was expecting a need to manually call the code in
        // `init()`,but that seem to happen automatically. So yet more I have learn.
        main()
    }
    
    NOTE: If main function does not exit the test will hang/loop.
  3. Then assert on the exit code returned from the sub-process.
    // get exit code.
    got := cmd.ProcessState.ExitCode()
    if got != test.want {
        t.Errorf("got %q, want %q", got, test.want)
    }
    
    NOTE: In this example, if anything other than the expected exit code is returned, the test outputs the STDOUT and or STDERR from the sub-process, for help with debugging.

See full example here: go-gitter: Testing the CLI

Share:
49,942
ThePiachu
Author by

ThePiachu

Computer Science double BSc, master-level student. Bitcoin enthusiast. My Google-powered Bitcoin Calculator: http://tpbitcalc.appspot.com/ My tweets: http://twitter.com/#!/ThePiachu If you have an iPhone and need some dice, check out my apps: http://tiny.cc/TPiDev

Updated on July 09, 2022

Comments

  • ThePiachu
    ThePiachu almost 2 years

    I want to test a few functions that are included in my main package, but my tests don't appear to be able to access those functions.

    My sample main.go file looks like:

    package main
    
    import (
        "log"
    )
    
    func main() {
        log.Printf(foo())
    }
    
    func foo() string {
        return "Foo"
    }
    

    and my main_test.go file looks like:

    package main
    
    import (
        "testing"
    )
    
    func Foo(t testing.T) {
        t.Error(foo())
    }
    

    when I run go test main_test.go I get

    # command-line-arguments
    .\main_test.go:8: undefined: foo
    FAIL    command-line-arguments [build failed]
    

    As I understand, even if I moved the test file elsewhere and tried importing from the main.go file, I couldn't import it, since it's package main.

    What is the correct way of structuring such tests? Should I just remove everything from the main package asides a simple main function to run everything and then test the functions in their own package, or is there a way for me to call those functions from the main file during testing?

  • Dave C
    Dave C almost 9 years
    Why are you specifying any files at all to go test? Don't.
  • David Budworth
    David Budworth almost 9 years
    true, but I figured the persona asking is struggling a bit and I didn't want to confuse the issue with setting GOPATH or anything like that. Make "go test ." would work? (haven't tried that outside of GOPATH before)
  • Admin
    Admin about 8 years
    You don't want your test file to be in the main package. You will end up with a lot of code and flags you didn't want if you do.
  • matthias krull
    matthias krull over 6 years
    The answer doesn't really answer the the question. It is just a complicated equivalent to calling go test without specifying files.
  • perrohunter
    perrohunter over 5 years
    but to achieve 100% coverage, the main function needs to be tested, how can this be achieved?
  • Zan Lynx
    Zan Lynx over 5 years
    @perrohunter 100% test coverage is a mirage. Don't bother with it. The thing I hate most about unrealistic test coverage policies is one time I added an if err to something like a file close it reduced the test coverage because we couldn't simulate a file close error. It may have been untested but that's 100% better than ignoring the error and pretending it can't happen!
  • Michael B.
    Michael B. about 5 years
    I don't want to start a code coverage rage war here, but I disagree with the statement that 100% coverage is a mirage. Pragmatically speaking, yes, there are times when the cost of delayed delivery of software due to satisfying a coverage policy outweighs the cost of not covering manually tested code. But when you say that you couldn't simulate a file close error, that's actually a design flaw. This is where interfaces come in handy in Go. For example, "func Foo(file os.File)" is not as easily tested as "func Foo(file io.ReadWriteCloser)"
  • user1016765
    user1016765 almost 3 years
    Calling main() to start your web application to run integration tests is a valid reason for ‘testing main’, and also useful if you’re using Cucumber to test behavior.
  • Joe B
    Joe B about 2 years
    FYI init() is automatically run for every package that's imported - not just main. Some libraries use it to set up stuff and will ask you to import it for those side effects: e.g. import _ "go.pkg/some/lib"