Testing a command-line gem

programming Ruby ruby testing cli

I’ve written many ruby gems that have binaries, generally with commander for the command-line interface. I was trying to increase the test coverage on those gems when I realized that it’s tricky to test the parts are integrated with commander.

This is what a common commander setup looks like:

require 'rubygems'
require 'commander/import'

program :name, 'Foo Bar'
program :version, '1.0.0'
program :description, 'Stupid command that prints foo or bar.'

command :foo do |c|
  # ...
end

Requiring that file from your gem’s binary does the trick.

#!/usr/bin/env ruby
require File.join(
  File.dirname(__FILE__), '..', 'lib', 'program'
)

Outstanding.

The main problem for testing is that this is meant for automatic execution. The commander/import require runs commander on exit as soon as it’s finished loading the file. But for testing, you want to run commander in a test, not when the file is required. And you definitely don’t want to have to play with un-requiring files to run it multiple times.

What you need is to have the commander program available as an object that you can test at will with any arguments. To achieve that, I looked at what commander/import does:

require 'commander'
require 'commander/delegates'

include Commander::UI
include Commander::UI::AskForClass
include Commander::Delegates

# ...

at_exit { run! }

What Commander::Delegates does is forward most commander methods (e.g. program, command) to a singleton Commander::Runner instance. So let’s do the same thing with a runner of our own.

require 'commander'

module FooBar
  class Program < Commander::Runner
    include Commander::UI
    include Commander::UI::AskForClass

    # No need to include Commander::Delegates,
    # as we're a Commander::Runner already.

    def initialize argv = ARGV
      super argv

      program :name, 'Foo Bar'
      program :version, '1.0.0'
      program :description,
        'Stupid command that prints foo or bar.'

      command :foo do |c|
        # ...
      end
    end
  end
end

You can now test your commander program to your heart’s content.

# test result
program = FooBar::Program.new custom_args
program.run!
expect(File.exists?('some file')).to be_true

# test errors
program = FooBar::Program.new bad_args
expect{ program.run! }.to raise_error(FooBar::Error)

# test output
program = FooBar::Program.new custom_args

stdout, stderr = StringIO.new, StringIO.new
$stdout, $stderr = stdout, stderr
program.run!
$stdout, $stderr = STDOUT, STDERR

expect(stdout.string.chomp("\n")).to eq('some output')
expect(stderr.string).to be_empty

Note that your binary must now run the commander program.

#!/usr/bin/env ruby
require File.join(
  File.dirname(__FILE__), '..',
  'lib', 'foo_bar', 'program'
)

FooBar::Program.new.run!

Unfortunately that doesn’t cover user interaction, but I don’t know how to test that yet. I will probably write another article when the time comes. In the meantime, enjoy.