go-cli-test-support

A Ruby gem that compiles Go CLI applications to binaries and enables testing with RSpec using a DSL-like syntax for command-line arguments.

RSpec test helper for Go CLI applications that simplifies testing by handling compilation, execution, and environment isolation automatically.

What is this?

This library is a helper for testing Go CLI applications with RSpec.

While you would normally use Go's testing package to test Go applications, this library allows you to:

  1. Compile your Go app to a binary
  2. Write command-line arguments using RSpec's DSL-like syntax
  3. Easily capture stdout/stderr/exit_code
  4. Completely isolate the environment (each test gets its own $HOME)
# You can write tests like this
exec('add', '10', '5')
expect(exit_code).to eq(0)
expect(stdout.strip).to eq('15.00')

Features

  • 🔨 Automatic compilation - Builds your Go CLI app before tests
  • 📦 Binary name detection - Extracts binary name from go.mod
  • 📁 Smart directory detection - Automatically finds cmd/ directory if present
  • 🔒 Environment isolation - Each test runs in isolated temp directory with custom $HOME
  • 🎯 Simple API - Easy setup/teardown and execution methods
  • Timeout control - Configurable timeout for command execution
  • 🐛 Debug mode - Detailed logging for troubleshooting
  • 🔧 Custom build flags - Support for any Go build options
  • Error handling - Clear error messages for common issues

Installation

Add this line to your application's Gemfile:

gem 'go-cli-test-support'

And then execute:

bundle install

Or install it yourself as:

gem install go-cli-test-support

Usage

Basic Example

require 'rspec'
require 'go-cli-test-support'

RSpec.describe 'My CLI App' do
  include GoTestHelper

  before(:each) do
    setup_go_test('/path/to/your/go/project')
  end

  after(:each) do
    teardown_go_test
  end

  it 'runs successfully' do
    exec('--version')
    expect(exit_code).to eq(0)
    expect(stdout).to include('1.0.0')
  end
end

Directory Structure Support

The helper supports multiple Go project structures:

Standard Structure

my-project/
├── go.mod
└── main.go

cmd/ Directory Structure

my-project/
├── go.mod
└── cmd/
    └── main.go

The helper automatically detects and uses the cmd/ directory if present.

API Reference

setup_go_test(project_dir, compile_path: nil, build_flags: [])

Prepares the test environment:

  • Creates isolated temporary directory
  • Compiles the Go binary
  • Detects binary name from go.mod

Parameters:

  • project_dir (String): Path to your Go project root (where go.mod is located)
  • compile_path (String, optional): Override the directory to compile from
  • build_flags (Array, optional): Custom Go build flags (e.g., -ldflags, -tags)

Raises:

  • ArgumentError: If project directory doesn't exist or go.mod is missing

Example:

# Auto-detect compile directory
setup_go_test('/path/to/project')

# Explicit compile path
setup_go_test('/path/to/project', compile_path: '/path/to/project/cmd')

# With custom build flags
setup_go_test('/path/to/project', build_flags: ['-ldflags', '-s -w'])

# Multiple options
setup_go_test(
  '/path/to/project',
  compile_path: '/path/to/project/cmd',
  build_flags: ['-v', '-tags', 'integration']
)

teardown_go_test

Cleans up the temporary test directory. Call this in after(:each) or after(:all).

exec(*args, stdin: nil, timeout: 30)

Executes the compiled binary with given arguments.

Parameters:

  • *args: Command-line arguments to pass to the binary
  • stdin (String, optional): Data to send to stdin
  • timeout (Integer, optional): Command timeout in seconds (default: 30)

Returns: self (for method chaining)

Timeout Behavior:

  • If command exceeds timeout, exit_code will be 124
  • stderr will contain timeout message
  • stdout will be empty

Example:

exec('add', 'task')
exec('list')
exec('input', stdin: "some data\n")

# Custom timeout
exec('long-command', timeout: 60)

# Check for timeout
exec('command', timeout: 5)
if exit_code == 124
  puts "Command timed out!"
end

Accessors

After calling exec, you can access:

  • stdout - Standard output from the command
  • stderr - Standard error from the command
  • exit_code - Exit code of the command
  • test_dir - Path to the isolated test directory

Debug Mode

Enable debug mode to see detailed execution information:

self.debug = true  # Enable debug output
self.debug = false # Disable debug output

When enabled, you'll see:

  • Build command details
  • Execution details
  • Exit codes
  • stdout/stderr content

Complete Example

require 'rspec'
require 'go-cli-test-support'

RSpec.describe 'Todo CLI' do
  include GoTestHelper

  let(:project_dir) { File.expand_path('../my-todo-app', __dir__) }

  before(:each) do
    setup_go_test(project_dir)
  end

  after(:each) do
    teardown_go_test
  end

  it 'shows usage when no arguments' do
    exec
    expect(exit_code).to eq(1)
    expect(stdout).to include('Usage:')
  end

  it 'can add and list todos' do
    exec('add', 'Buy milk')
    expect(exit_code).to eq(0)
    expect(stdout).to include('Added')

    exec('list')
    expect(stdout).to include('Buy milk')
  end

  it 'isolates data between tests' do
    # Each test gets a fresh $HOME directory
    exec('add', 'Task 1')

    data_file = File.join(test_dir, 'todos.json')
    expect(File.exist?(data_file)).to be true
  end
end

Advanced Usage

Debug Mode

RSpec.describe 'My App' do
  include GoTestHelper

  before(:each) do
    self.debug = true  # Enable detailed logging
    setup_go_test(project_dir)
  end

  it 'debugs execution' do
    exec('command')
    # [GoTestHelper DEBUG] Executing: /tmp/app_test_xxx/app command
    # [GoTestHelper DEBUG] Exit code: 0
  end
end

Custom Build Flags

before(:each) do
  # Production build with optimizations
  setup_go_test(
    project_dir,
    build_flags: ['-ldflags', '-s -w', '-tags', 'production']
  )
end

Timeout Handling

it 'handles long-running commands' do
  exec('backup', timeout: 300)  # 5 minutes

  if exit_code == 124
    puts "Backup timed out"
  end
end

Error Handling

it 'handles setup errors gracefully' do
  expect {
    setup_go_test('/invalid/path')
  }.to raise_error(ArgumentError, /does not exist/)
end

How It Works

  1. Compilation: Compiles your Go app to a temporary directory
  2. Binary Naming: Extracts the binary name from your go.mod module path (e.g., github.com/user/my-appmy-app)
  3. Isolation: Sets $HOME to the test directory, isolating config files and data
  4. Execution: Runs the binary with provided arguments and captures output

Requirements

  • Ruby >= 2.7.0
  • Go installed and available in PATH
  • RSpec ~> 3.12

Development

After checking out the repo, run:

bundle install
rspec

Contributing

Bug reports and pull requests are welcome on GitHub.

License

The gem is available as open source under the terms of the MIT License.