diff --git a/VERSION b/VERSION index 5a5831a..d169b2f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.7 +0.0.8 diff --git a/cmd/mock/main_test.go b/cmd/mock/main_test.go index a2b41a8..fd057b3 100644 --- a/cmd/mock/main_test.go +++ b/cmd/mock/main_test.go @@ -1,53 +1,19 @@ package main import ( - "errors" - "os" - "os/exec" "testing" - "github.com/stretchr/testify/assert" "github.com/tkrop/go-testing/test" ) -type MainParams struct { - args []string - expectExitCode int -} - -var testMainParams = map[string]MainParams{ - "no mock to generate": { - args: []string{"mock"}, - expectExitCode: 0, +var testMainParams = map[string]test.MainParams{ + "no mocks": { + Args: []string{"mock"}, + Env: []string{}, + ExitCode: 0, }, } func TestMain(t *testing.T) { - test.Map(t, testMainParams). - Run(func(t test.Test, param MainParams) { - // Switch to execute main function in test process. - if name := os.Getenv("TEST"); name != "" { - // Ensure only expected test is running. - if name == t.Name() { - os.Args = param.args - main() - assert.Fail(t, "os-exit not called") - } - // Skip other test. - return - } - - // Call the main function in a separate process to prevent capture - // regular process exit behavior. - cmd := exec.Command(os.Args[0], "-test.run=TestMain") - cmd.Env = append(os.Environ(), "TEST="+t.Name()) - if err := cmd.Run(); err != nil || param.expectExitCode != 0 { - errExit := &exec.ExitError{} - if errors.As(err, &errExit) { - assert.Equal(t, param.expectExitCode, errExit.ExitCode()) - } else { - assert.Fail(t, "unexpected error", err) - } - } - }) + test.Map(t, testMainParams).Run(test.TestMain(main)) } diff --git a/test/README.md b/test/README.md index d17073c..e7345ab 100644 --- a/test/README.md +++ b/test/README.md @@ -164,7 +164,7 @@ func TestUnit(t *testing.T) { } ``` -**Note:** To enable panic testing, the isolated test enviroment is recovering +**Note:** To enable panic testing, the isolated test environment is recovering from all panics by default and converting it in a fatal error message. This is often most usable and sufficient to fix the issue. If you need to discover the source of the panic, you need to spawn a new unrecovered go-routine. @@ -173,4 +173,32 @@ source of the panic, you need to spawn a new unrecovered go-routine. hard to recreate. Do not try it. +## Out-of-the-box test pattners + +Currently, the package supports the following _out-of-the-box_ test pattern for +testing of `main`-methods of commands. + +```go +testMainParams := map[string]test.MainParams{ + "no mocks": { + Args: []string{"mock"}, + Env: []string{}, + ExitCode: 0, + }, +} + +func TestMain(t *testing.T) { + test.Map(t, testMainParams).Run(test.TestMain(main)) +} +``` + +The pattern executes a the `main`-method in a separate process that allows to +setup the command line arguments (`Args`) as well as to modify the environment +variables (`Env`) and to capture and compare the exit code. + +**Note:** the general approach can be used to test any code calling `os.Exit`, +however, it is focused on testing methods without arguments parsing command +line arguments, i.e. in particular `func main() { ... }`. + + [gomock]: diff --git a/test/pattern.go b/test/pattern.go new file mode 100644 index 0000000..6e9b97f --- /dev/null +++ b/test/pattern.go @@ -0,0 +1,67 @@ +package test + +import ( + "errors" + "os" + "os/exec" + + "github.com/stretchr/testify/require" +) + +// MainParams provides the test parameters for testing a `main`-method. +type MainParams struct { + Args []string + Env []string + ExitCode int +} + +// TestMain creates a test function that runs the given `main`-method in a +// separate test process to allow capturing the exit code and checking it +// against the expectation. This can be applied as follows: +// +// testMainParams := map[string]test.MainParams{ +// "no mocks": { +// Args: []string{"mock"}, +// Env: []string{}, +// ExitCode: 0, +// }, +// } +// +// func TestMain(t *testing.T) { +// test.Map(t, testMainParams).Run(test.TestMain(main)) +// } +// +// The test method is spawning a new test using the `TEST` environment variable +// to select the expected parameter set containing the command line arguments +// (`Args`) to execute in the spawned process. The test instance is also called +// setting the given additional environment variables (`Env`) to allow +// modification of the test environment. +func TestMain(main func()) func(t Test, param MainParams) { + return func(t Test, param MainParams) { + // Switch to execute main function in test process. + if name := os.Getenv("TEST"); name != "" { + // Ensure only expected test is running. + if name == t.Name() { + os.Args = param.Args + main() + require.Fail(t, "os-exit not called") + } + // Skip unexpected tests. + return + } + + // Call the main function in a separate process to prevent capture + // regular process exit behavior. + // #nosec G204 -- secured by calling only the test instance. + cmd := exec.Command(os.Args[0], "-test.run="+t.(*Tester).t.Name()) + cmd.Env = append(append(os.Environ(), "TEST="+t.Name()), param.Env...) + if err := cmd.Run(); err != nil || param.ExitCode != 0 { + errExit := &exec.ExitError{} + if errors.As(err, &errExit) { + require.Equal(t, param.ExitCode, errExit.ExitCode()) + } else { + require.Fail(t, "unexpected error", err) + } + } + } +}