How to test os.exit scenarios in Go

后端 未结 5 1823
渐次进展
渐次进展 2020-12-23 12:09

Given this code

func doomed() {
  os.Exit(1)
}

How do I properly test that calling this function will result in an exit using go test

相关标签:
5条回答
  • 2020-12-23 12:11

    Code for testing:

    package main
    import "os"
    
    var my_private_exit_function func(code int) = os.Exit
    
    func main() {
        MyAbstractFunctionAndExit(1)
    }
    
    func MyAbstractFunctionAndExit(exit int) {
        my_private_exit_function(exit)
    }
    

    Testing code:

    package main
    
    import (
        "os"
        "testing"
    )
    
    func TestMyAbstractFunctionAndExit(t *testing.T) {
        var ok bool = false // The default value can be omitted :)
    
        // Prepare testing
        my_private_exit_function = func(c int) {
            ok = true
        }
        // Run function
        MyAbstractFunctionAndExit(1)
        // Check
        if ok == false {
            t.Errorf("Error in AbstractFunction()")
        }
        // Restore if need
        my_private_exit_function = os.Exit
    }
    
    0 讨论(0)
  • 2020-12-23 12:12

    You can't, you would have to use exec.Command and test the returned value.

    0 讨论(0)
  • 2020-12-23 12:14

    There's a presentation by Andrew Gerrand (one of the core members of the Go team) where he shows how to do it.

    Given a function (in main.go)

    package main
    
    import (
        "fmt"
        "os"
    )
    
    func Crasher() {
        fmt.Println("Going down in flames!")
        os.Exit(1)
    }
    

    here's how you would test it (through main_test.go):

    package main
    
    import (
        "os"
        "os/exec"
        "testing"
    )
    
    func TestCrasher(t *testing.T) {
        if os.Getenv("BE_CRASHER") == "1" {
            Crasher()
            return
        }
        cmd := exec.Command(os.Args[0], "-test.run=TestCrasher")
        cmd.Env = append(os.Environ(), "BE_CRASHER=1")
        err := cmd.Run()
        if e, ok := err.(*exec.ExitError); ok && !e.Success() {
            return
        }
        t.Fatalf("process ran with err %v, want exit status 1", err)
    }
    

    What the code does is invoke go test again in a separate process through exec.Command, limiting execution to the TestCrasher test (via the -test.run=TestCrasher switch). It also passes in a flag via an environment variable (BE_CRASHER=1) which the second invocation checks for and, if set, calls the system-under-test, returning immediately afterwards to prevent running into an infinite loop. Thus, we are being dropped back into our original call site and may now validate the actual exit code.

    Source: Slide 23 of Andrew's presentation. The second slide contains a link to the presentation's video as well. He talks about subprocess tests at 47:09

    0 讨论(0)
  • 2020-12-23 12:15

    I do this by using bouk/monkey:

    func TestDoomed(t *testing.T) {
      fakeExit := func(int) {
        panic("os.Exit called")      
      }
      patch := monkey.Patch(os.Exit, fakeExit)
      defer patch.Unpatch()
      assert.PanicsWithValue(t, "os.Exit called", doomed, "os.Exit was not called")
    }
    

    monkey is super-powerful when it comes to this sort of work, and for fault injection and other difficult tasks. It does come with some caveats.

    0 讨论(0)
  • 2020-12-23 12:24

    I don't think you can test the actual os.Exit without simulating testing from the outside (using exec.Command) process.

    That said, you might be able to accomplish your goal by creating an interface or function type and then use a noop implementation in your tests:

    Go Playground

    package main
    
    import "os"
    import "fmt"
    
    type exiter func (code int)
    
    func main() {
        doExit(func(code int){})
        fmt.Println("got here")
        doExit(func(code int){ os.Exit(code)})
    }
    
    func doExit(exit exiter) {
        exit(1)
    }
    
    0 讨论(0)
提交回复
热议问题