Testing os.Exit scenarios in Go with coverage information (coveralls.io/Goveralls)

With a slight refactoring, you may easily achieve 100% coverage.

foo/bar.go:

package foo

import (
    "fmt"
    "os"
)

var osExit = os.Exit

func Crasher() {
    fmt.Println("Going down in flames!")
    osExit(1)
}

And the testing code: foo/bar_test.go:

package foo

import "testing"

func TestCrasher(t *testing.T) {
    // Save current function and restore at the end:
    oldOsExit := osExit
    defer func() { osExit = oldOsExit }()

    var got int
    myExit := func(code int) {
        got = code
    }

    osExit = myExit
    Crasher()
    if exp := 1; got != exp {
        t.Errorf("Expected exit code: %d, got: %d", exp, got)
    }
}

Running go test -cover:

Going down in flames!
PASS
coverage: 100.0% of statements
ok      foo        0.002s

Yes, you might say this works if os.Exit() is called explicitly, but what if os.Exit() is called by someone else, e.g. log.Fatalf()?

The same technique works there too, you just have to switch log.Fatalf() instead of os.Exit(), e.g.:

Relevant part of foo/bar.go:

var logFatalf = log.Fatalf

func Crasher() {
    fmt.Println("Going down in flames!")
    logFatalf("Exiting with code: %d", 1)
}

And the testing code: TestCrasher() in foo/bar_test.go:

func TestCrasher(t *testing.T) {
    // Save current function and restore at the end:
    oldLogFatalf := logFatalf
    defer func() { logFatalf = oldLogFatalf }()

    var gotFormat string
    var gotV []interface{}
    myFatalf := func(format string, v ...interface{}) {
        gotFormat, gotV = format, v
    }

    logFatalf = myFatalf
    Crasher()
    expFormat, expV := "Exiting with code: %d", []interface{}{1}
    if gotFormat != expFormat || !reflect.DeepEqual(gotV, expV) {
        t.Error("Something went wrong")
    }
}

Running go test -cover:

Going down in flames!
PASS
coverage: 100.0% of statements
ok      foo     0.002s

Leave a Comment