0

I'm trying to write a unit test in go for a function that returns non-zero exit codes. I'm developing a CLI app with cobra to validate semantic versions. In case a validation fails, I return some information in JSON and exit with os.Exit(1).

Now I want to test if this really works as intended. In my test I pass with one data set that should success and return 0 and one that should fail and return 1. But the test that should return 1 always cancels the test and hence cancels all following iterations. This is my code:

func Test_ShouldGetCorrectExitCode(t *testing.T) {
    testCases := []struct {
        args          []string
        shouldBeValid bool
    }{
        {[]string{"0.1.0"}, false},
        {[]string{"v0.1.0"}, true},
    }
    for _, tc := range testCases {
        assert := assert.New(t)

        cmd := NewCmdValidate()
        cmd.SetArgs(tc.args)
        err := cmd.Execute()

        assert.Nil(err)
    }
}

So far the assertions are not really sophisticated because I don't get the test to run the way I expect. Anyone got an idea how I can test for exit codes in go?

Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189
  • You should use a function variable initially set to `os.Exit`, and replace in tests. See possible duplicate: [Testing os.Exit scenarios in Go with coverage information (coveralls.io/Goveralls)](https://stackoverflow.com/questions/40615641/testing-os-exit-scenarios-in-go-with-coverage-information-coveralls-io-goverall/40801733#40801733) – icza Aug 08 '23 at 12:35
  • `t.Error*` and [`t.Fail`](https://pkg.go.dev/testing#T.Fail) methods mark the test as failing but do not stop execution. – JimB Aug 08 '23 at 12:37
  • 1
    You're using `testify`, do you? AFAIK, the stock Go's `testing` does not have any package with the name `assert`. If yes, `assert.*` do not stop execution, only `require.*` do, so your question needs further clarification. – kostix Aug 08 '23 at 14:01
  • 1
    [Observe](https://go.dev/play/p/2J0qYpY4bqO) that plain faliure of an `assert.*` test in a loop iteration does not terminate the whole test (in the example, all loop iterations run). – kostix Aug 08 '23 at 14:13

1 Answers1

4

The best solution is to not call os.Exit from your function under test. A good practice can be to only call os.Exit from main(), and instead have your function return an exit status, which you can easily test for. Example:

func main() {
    os.Exit(run())
}

func run() int {
  /* Things to test here */
}

Then in your test:

func TestRun(t *testing.T) {
    t.Run("should succeed", func(t *testing.T) {
        status := run()
        if status != 0 {
            t.Errorf("Unexpected exit status: %d", status)
    })
    t.Run("should fail", func(t *testing.T) {
        status := run()
        if status != 1 {
            t.Errorf("Unexpected status code: %d", status)
        }
    })
}
Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189
  • 1
    More about this approach in [this post](https://pace.dev/blog/2020/02/12/why-you-shouldnt-use-func-main-in-golang-by-mat-ryer.html) by Mat Ryer. – jub0bs Aug 08 '23 at 14:18
  • 1
    That is it. Thanks. This way my code is cleaner and I don't test framework implementations. I only test my own implementation. Since I trust the framework to work (otherwise I would not have chosen it) I don't need to test framework features. They (should) do it. I only need to test my expected behavior. – Sebastian Sommerfeld Aug 09 '23 at 08:23