How to test os.exit scenarios in Go

24,466

Solution 1

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

Solution 2

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.

Solution 3

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)
}

Solution 4

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

Solution 5

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
}
Share:
24,466

Related videos on Youtube

mbrevoort
Author by

mbrevoort

@mbrevoort

Updated on July 09, 2022

Comments

  • mbrevoort
    mbrevoort almost 2 years

    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? This needs to occur within a suite of tests, in other words the os.Exit() call cannot impact the other tests and should be trapped.

    • ches
      ches almost 8 years
      Of course this isn't a direct answer to the question, and that's why I'm not writing it as one, but generally: avoid writing code like this. If you only Exit "at the end of the world" (main), like this pattern, then you won't be stuck writing such painful tests as the (good) accepted solution here. I fully acknowledge you may have been stuck testing someone else's code you couldn't readily refactor, but just hoping the advice is helpful to future readers…
    • ches
      ches almost 8 years
      If you do follow that pattern and you happen to use Gomega, it has a pretty cool gexec package that is nice for testing results of executables in a black box manner.
  • 030
    030 over 8 years
    Running the test results in: process ran with err exec: "cmd": executable file not found in $PATH, want exit status 1
  • 030
    030 over 8 years
    Could you add the test as well please?
  • Timo Reimann
    Timo Reimann over 8 years
    @Alfred Did you keep the implementation and tests in separate files, e.g., main.go and main_test.go, respectively? I amended my answer and double-checked that it works on my machine.
  • 030
    030 over 8 years
    Yes. They are separated. Could it be possible that something is wrong with some environment variables go env? I always use go test for testing and I have created several tests to test other files as well.
  • Timo Reimann
    Timo Reimann almost 8 years
    Sorry for not getting back to you, I totally lost track of this. In case it is still somewhat relevant to you: Can you take a closer look at what cmd looks for you? Specifically, does the path look reasonable?
  • Daniel YC Lin
    Daniel YC Lin over 6 years
    This method can not let the -cover show your lines have tested
  • Timo Reimann
    Timo Reimann over 6 years
    That is true, unfortunately. To my knowledge, the only way to mitigate that problem is to minimize the need for integration-testing executables and focus on unit tests instead. What I tend to do these days is have my executable call some function as soon as possible and focus on unit-testing that function.
  • Adam M-W
    Adam M-W almost 6 years
    For reference, the license of bouk/monkey is likely incompatible with your project: github.com/bouk/monkey/pull/18
  • Allen Luce
    Allen Luce almost 6 years
    You shouldn't use any software without being comfortable with all the risks, including the legal ones. You also shouldn't assume you can understand a software license, this or any other, just by reading it.
  • TheDiveO
    TheDiveO about 4 years
    For code coverage of my tests requiring reexecution for entering mount namespaces I came up solution which automatically merges the separate code coverages from the re-executions into the final coverage; please see github.com/thediveo/gons and here its gons/reexec and gons/reexec/testing packages. The reexec code is geared towards namespaced re-entry, but you'll quickly see how to make your own re-exec without namespace support, but using the profile merging.
  • Olli
    Olli over 3 years
    Indeed, considering the license, no-one should be using this. In this case understanding the license is very easy and straightforward. Even though the code is available, the license does not allow using it. In your personal, private projects that has no practical meaning but for anything else it matters.
  • Helyrk
    Helyrk over 3 years
    To make it more clear, this answer suggests to put the os.Exit in a variable and call it through this variable in the production code, while in testing you can replace its value with another function that wouldn't exit but would let you know if it was called. This is a valid solution if you can afford to modify your production code to improve its testability.
  • glemiere
    glemiere almost 3 years
    This works like magic for error code 1, but how would you go about detecting a hard coded exit(0)?
  • thenamewasmilo
    thenamewasmilo almost 3 years
    That license is not valid in the European Union, where this guy resides. On the other hand I would not use it in any way shape or form, and change the way my program works. Besides it being incredibly dirty what he is doing I would not feel comfortable relying on a project made by a developer who is clearly not acting in a rational or professional manner and that is just a left-pad situation waiting to happen. reuters.com/article/…
  • thenamewasmilo
    thenamewasmilo almost 3 years
    However, if you are going to panic anyway, and you like dirty code, you could do this, which I am told I should not have written, but I like it :p I would not advice it, or if you do like it, copy the code, because I don't maintain it in a stable way, and change things all the time. Another option, possibly better still would be to use the re-exec trick the docker guys developed since that is actually maintained code that is used in a real project. I would go with that... github.com/TheApeMachine/errnie/blob/master/guard.go?ts=4
  • Gus
    Gus over 2 years
    Beware that the -test.run=TestCrasher flag matches a regexp, so it will run any other tests that contain TestCrasher in their name (docs). To run only TestCrasher you could use -test.run=^TestCrasher$
  • Alaska
    Alaska over 2 years
    This is not good practice; to have the power to change an exit scenario through a global variable.