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

只愿长相守 提交于 2019-11-27 13:42:36

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

Interfaces and mocks

Using Go interfaces possible to create mock-able compositions. A type could have interfaces as bound dependencies. These dependencies could be easily substituted with mocks appropriate to the interfaces.

type Exiter interface {
    Exit(int)
}

type osExit struct {}

func (o* osExit) Exit (code int) {
    os.Exit(code)
}

type Crasher struct {
    Exiter
}

func (c *Crasher) Crash() {
    fmt.Println("Going down in flames!")
    c.Exit(1)
}

Testing

type MockOsExit struct {
    ExitCode int
}

func (m *MockOsExit) Exit(code int){
    m.ExitCode = code
}

func TestCrasher(t *testing.T) {
    crasher := &Crasher{&MockOsExit{}}
    crasher.Crash() // This causes os.Exit(1) to be called
    f := crasher.Exiter.(*MockOsExit)
    if f.ExitCode == 1 {
        fmt.Printf("Error code is %d\n", f.ExitCode)
        return
    }
    t.Fatalf("Process ran with err code %d, want exit status 1", f.ExitCode)
}

Disadvantages

Original Exit method still won't be tested so it should be responsible only for exit, nothing more.

Functions are first class citizens

Parameter dependency

Functions are first class citizens in Go. A lot of operations are allowed with functions so we can do some tricks with functions directly.

Using 'pass as parameter' operation we can do a dependency injection:

type osExit func(code int)

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

Testing:

var exit_code int 
func os_exit_mock(code int) {
     exit_code = code
}

func TestCrasher(t *testing.T) {

    Crasher(os_exit_mock) // This causes os.Exit(1) to be called
    if exit_code == 1 {
        fmt.Printf("Error code is %d\n", exit_code)
        return
    }
    t.Fatalf("Process ran with err code %v, want exit status 1", exit_code)
}

Disadvantages

You must pass a dependency as a parameter. If you have many dependencies a length of params list could be huge.

Variable substitution

Actually it is possible to do it using "assign to variable" operation without explicit passing a function as a parameter.

var osExit = os.Exit

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

Testing

var exit_code int
func osExitMock(code int) {
    exit_code = code
}

func TestCrasher(t *testing.T) {
    origOsExit := osExit
    osExit = osExitMock
    // Don't forget to switch functions back!
    defer func() { osExit = origOsExit }()

    Crasher()
    if exit_code != 1 {
        t.Fatalf("Process ran with err code %v, want exit status 1", exit_code)
    }
}

disadvantages

It is implicit and easy to crash.

Design notes

If you plan to declare some logic below Exit an exit logic must be isolated with else block or extra return after exit because mock won't stop execution.

func (c *Crasher) Crash() {
    if SomeCondition == true {
        fmt.Println("Going down in flames!")
        c.Exit(1)  // Exit in real situation, invoke mock when testing
    } else {
        DoSomeOtherStuff()
    }

}
dmportella

It not common practice to put tests around the Main function of an application in GOLANG specifically because of issues like that. There was a question that is already answered that touched this same issue.

showing coverage of functional tests without blind spots

To Summarize

To summarize it you should avoid putting tests around the main entry point of the application and try to design your application in way that little code is on the Main function so it decoupled enough to allow you to test as much of your code as possible.

Check GOLANG Testing for more information.

Coverage to 100%

As I detailed on on the previous answer since is a bad idea to try getting tests around the Main func and the best practice is to put as little code there as possible so it can be tested properly with out blind spots it stands to reason that trying to get 100% coverage while trying to include the Main func is wasted effort so it better to ignore it in the tests.

You can use build tags to exclude the main.go file from the tests therefore reaching your 100% coverage or all green.

Check: showing coverage of functional tests without blind spots

If you design your code well and keep all the actual functionality well decoupled and tested having a few lines of code that do very little other then calling the actual pieces of code that do all the actual work and are well tested it does't really matter that you are not testing a tiny and not significant code.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!