byob-go-cli

Three-part command shape with a runF test-injection hook

byob-command-shape.1 command-shape

Problem: a cobra command that parses flags, opens resources, and executes business logic in one RunE function is untestable and unreadable. Even once you split it, testing the parsing without executing the business logic still wants a seam — otherwise every flag-parsing test sets up (or mocks) every dependency and asserts on side effects, which is integration-test territory.

Idea: split every subcommand into three pieces. (1) Options struct — holds dependencies (pulled from Factory) and parsed flag values. (2) NewCmdXxx(f *Factory, runF func(*Options) error) *cobra.Command — binds flags, constructs Options, wires RunE. The runF parameter is a test-injection hook: inside RunE, if runF != nil { return runF(opts) } takes the test path. Production code passes nil; tests pass a closure that captures the parsed Options. (3) A package-private xxxRun(opts *Options) error — pure business logic.

Each layer is independently testable. Flag-parsing tests call the constructor with a runF that captures Options and returns nil — no real work executes. Business-logic tests call xxxRun with a handcrafted Options.

Tradeoffs: three functions per command instead of one, plus an extra parameter on every constructor. You give up the ability to export the runFunc directly. Small file tax that pays back immediately when you write the first test.

Design

type Options struct {
    IO    *iostreams.IOStreams
    Store func() (Store, error)
    Name  string
    Force bool
}

func NewCmdCreate(f *Factory, runF func(*Options) error) *cobra.Command {
    opts := &Options{IO: f.IOStreams, Store: f.Store}
    cmd := &cobra.Command{
        Use:  "create <name>",
        Args: cobra.ExactArgs(1),
        RunE: func(c *cobra.Command, args []string) error {
            opts.Name = args[0]
            if runF != nil { return runF(opts) }
            return createRun(opts)
        },
    }
    cmd.Flags().BoolVarP(&opts.Force, "force", "f", false, "overwrite existing")
    return cmd
}

func createRun(opts *Options) error { /* pure business logic */ }

Production wiring vs. test wiring:

// production:
cmd := NewCmdCreate(f, nil)

// test:
var got *Options
cmd := NewCmdCreate(f, func(o *Options) error { got = o; return nil })
cmd.SetArgs([]string{"myname", "--force"})
err := cmd.Execute()

require.NoError(t, err)
require.Equal(t, "myname", got.Name)
require.True(t, got.Force)