Ahnii!

Ever struggled with testing Cobra CLI applications? Dependency injection makes it dramatically easier. This post walks through the approach with practical examples.

Why Dependency Injection?

Key benefits for CLI apps:

  • Easier to mock dependencies
  • More testable code
  • Cleaner separation of concerns
  • Flexible configuration

Basic Setup

Here’s our basic CLI structure with DI:

type AppDependencies struct {
    Logger  Logger
    Config  Config
    Client  HTTPClient
}

func NewRootCmd(deps *AppDependencies) *cobra.Command {
    cmd := &cobra.Command{
        Use:   "mycli",
        Short: "My CLI application",
        RunE: func(cmd *cobra.Command, args []string) error {
            return runRoot(deps, args)
        },
    }
    return cmd
}

Testing Strategy

  1. Mock Dependencies
type MockLogger struct {
    mock.Mock
}

func TestRootCommand(t *testing.T) {
    mockLogger := &MockLogger{}
    deps := &AppDependencies{
        Logger: mockLogger,
    }
    
    cmd := NewRootCmd(deps)
    assert.NotNil(t, cmd)
}
  1. Test Command Execution
func TestCommandExecution(t *testing.T) {
    deps := setupTestDependencies()
    cmd := NewRootCmd(deps)
    
    output, err := executeCommand(cmd, "arg1", "--flag=value")
    assert.NoError(t, err)
    assert.Contains(t, output, "expected output")
}

Best Practices

  • Keep dependencies minimal and focused
  • Use interfaces for flexibility
  • Test edge cases thoroughly
  • Mock external services

Common Patterns

  1. Configuration Injection
func NewConfig() *Config {
    return &Config{
        // Default values
    }
}
  1. Logger Injection
type Logger interface {
    Info(msg string, args ...interface{})
    Error(msg string, args ...interface{})
}

Dependency injection might seem like overhead at first, but it pays off in testability and maintainability. Start small and refactor as needed.

Baamaapii