3.7 KiB
3.7 KiB
Testing Cobra Commands
Basic test pattern
func TestServeCmd(t *testing.T) {
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
rootCmd.SetOut(stdout)
rootCmd.SetErr(stderr)
rootCmd.SetArgs([]string{"serve", "--port", "9090", "--dry-run"})
err := rootCmd.Execute()
require.NoError(t, err)
assert.Contains(t, stdout.String(), "listening on :9090")
assert.Empty(t, stderr.String())
}
Isolation between tests
Cobra accumulates flag state across Execute() calls on the same command instance. Tests must be isolated.
Option 1: Re-create the command tree per test (recommended for unit tests)
func newRootCmd() *cobra.Command {
root := &cobra.Command{Use: "myapp", SilenceUsage: true, SilenceErrors: true}
root.AddCommand(newServeCmd())
return root
}
func TestServeCmd(t *testing.T) {
root := newRootCmd()
root.SetArgs([]string{"serve", "--port", "9090"})
err := root.Execute()
require.NoError(t, err)
}
Option 2: Reset flags between tests
func TestWithReset(t *testing.T) {
t.Cleanup(func() {
rootCmd.ResetFlags()
// re-define flags if needed
})
}
Re-creating is safer — ResetFlags only clears the flag set, not subcommand state.
Testing commands that write output
Commands must use cmd.OutOrStdout() / cmd.ErrOrStderr() instead of os.Stdout / os.Stderr for this to work.
// In command handler:
func runServe(cmd *cobra.Command, args []string) error {
fmt.Fprintln(cmd.OutOrStdout(), "Server started")
fmt.Fprintln(cmd.ErrOrStderr(), "Debug: listening on port 8080")
return nil
}
// In test:
buf := new(bytes.Buffer)
rootCmd.SetOut(buf)
rootCmd.Execute()
assert.Contains(t, buf.String(), "Server started")
Golden file tests
For commands with structured or lengthy output, use golden files:
func TestOutputFormat(t *testing.T) {
buf := new(bytes.Buffer)
rootCmd.SetOut(buf)
rootCmd.SetArgs([]string{"list", "--output", "json"})
require.NoError(t, rootCmd.Execute())
golden := "testdata/list-json.golden"
if *update { // -update flag
os.WriteFile(golden, buf.Bytes(), 0644)
}
want, _ := os.ReadFile(golden)
assert.Equal(t, string(want), buf.String())
}
Run with -update to regenerate golden files after intentional output changes.
Testing error paths
func TestInvalidArgs(t *testing.T) {
stderr := new(bytes.Buffer)
rootCmd.SetErr(stderr)
rootCmd.SetArgs([]string{"delete"}) // missing required arg
err := rootCmd.Execute()
assert.Error(t, err)
assert.Contains(t, err.Error(), "accepts 1 arg")
}
Table-driven command tests
tests := []struct {
name string
args []string
wantOut string
wantErr bool
}{
{"no flags", []string{"serve"}, "listening on :8080", false},
{"custom port", []string{"serve", "--port", "9090"}, "listening on :9090", false},
{"invalid port", []string{"serve", "--port", "abc"}, "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
root := newRootCmd() // fresh command tree per test
buf := new(bytes.Buffer)
root.SetOut(buf)
root.SetArgs(tt.args)
err := root.Execute()
if tt.wantErr {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Contains(t, buf.String(), tt.wantOut)
}
})
}
Testing completions
func TestCompletion(t *testing.T) {
root := newRootCmd()
buf := new(bytes.Buffer)
root.SetOut(buf)
root.SetArgs([]string{"__complete", "delete", ""})
root.Execute()
assert.Contains(t, buf.String(), "pod")
assert.Contains(t, buf.String(), "service")
}