Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to test stdin for a CLI using rspec

I'm making a small Ruby program and can't figure out how to write RSpec specs that simulate multiple user command line inputs (the functionality itself works). I think this StackOverflow answer probably covers ground that is closest to where I am, but it's not quite what I need. I am using Thor for the command line interface, but I don't think this is an issue with anything in Thor.

The program can read in commands either from a file or the command line, and I've been able to successfully write tests to read in an execute them. Here's some code:

cli.rb

class CLI < Thor
  # ...
  method_option :filename, aliases: ['-f'],
                desc: "name of the file containing instructions",
                banner: 'FILE'

  desc "execute commands", "takes actions as per commands"
  def execute
    thing = Thing.new
    instruction_set do |instructions|
      instructions.each do |instruction|
        command, args = parse_instruction(instruction) # private helper method
        if valid_command?(command, args) # private helper method
          response = thing.send(command, *args)
          puts format(response) if response
        end
      end
    end
  end

  # ...

  no_tasks do
    def instruction_set
      if options[:filename]
        yield File.readlines(options[:filename]).map { |a| a.chomp }
      else
        puts usage
        print "> "
        while line = gets
          break if line =~ /EXIT/i
          yield [line]
          print "> "
        end
      end
    end
    # ..
  end

I've tested successfully for executing commands contained in a file with this code:

spec/cli_spec.rb

describe CLI do

  let(:cli) { CLI.new }

  subject { cli }

  describe "executing instructions from a file" do
    let(:default_file) { "instructions.txt" }
    let(:output) { capture(:stdout) { cli.execute } }

    context "containing valid test data" do
      valid_test_data.each do |data|
        expected_output = data[:output]

        it "should parse the file contents and output a result" do
          cli.stub(:options) { { filename: default_file } } # Thor options hash
          File.stub(:readlines).with(default_file) do
            StringIO.new(data[:input]).map { |a| a.strip.chomp }
          end
          output.should == expected_output
        end
      end
    end
  end
  # ...
end

and the valid_test_data referred to above is in the following form:

support/utilities.rb

def valid_test_data
  [
    {
      input: "C1 ARGS\r
              C2\r
              C3\r
              C4",
      output: "OUTPUT\n"
    }
    # ...
  ]
end

What I want to do now is exactly the same thing but instead of reading each command from the 'file' and executing it, I want to somehow simulate a user typing in to stdin. The code below is utterly wrong, but I hope it can convey the direction I want to go.

spec/cli_spec.rb

# ...
# !!This code is wrong and doesn't work and needs rewriting!!
describe "executing instructions from the command line" do
  let(:output) { capture(:stdout) { cli.execute } }

  context "with valid commands" do
    valid_test_data.each do |data|
      let(:expected_output) { data[:output] }
      let(:commands) { StringIO.new(data[:input]).map { |a| a.strip } }

      it "should process the commands and output the results" do
        commands.each do |command|
          cli.stub!(:gets) { command }
          if command == :report
            STDOUT.should_receive(:puts).with(expected_output)
          else
            STDOUT.should_receive(:puts).with("> ")
          end
        end
        output.should include(expected_output)
      end
    end
  end
end

I've tried using cli.stub(:puts) in various places, and generally rearranging this code around a lot, but can't seem to get any of my stubs to put data in stdin. I don't know if I can parse the set of inputs I expect from the command line in the same way as I do with a file of commands, or what code structure I should be using to solve this issue. If someone who has spec-ed up command-line apps could chime in, that would be great. Thanks.

like image 703
Paul Fioravanti Avatar asked Dec 21 '22 15:12

Paul Fioravanti


2 Answers

Rather than stubbing the universe, I think a few bits of indirection would help you write a unit test for this code. The simplest thing you can do is to allow the IO object for output to be injected, and default it to STDOUT:

class CLI < Thor
  def initialize(stdout=STDOUT)
    @stdout = stdout
  end

  # ...

  @stdout.puts # instead of raw puts
end

Then, in your tests, you can use a StringIO to examine what was printed:

let(:stdout) { StringIO.new }
let(:cli) { CLI.new(stdout) }

Another option is to use Aruba or something like it, and write full-on integration tests, where you actually run your program. This has other challenges as well (such as being nondestructive and making sure not to squash/use system or user files), but will be a better test of your app.

Aruba is Cucumber, but the assertions can use RSPec matchers. Or, you can use Aruba's Ruby API (which is undocumented, but public and works great) to do this without the hassle of Gherkin.

Either way, you will benefit from making your code a bit more test-friendly.

like image 65
davetron5000 Avatar answered Dec 23 '22 03:12

davetron5000


I ended up finding a solution that I think fairly closely mirrors the code for executing instructions from a file. I overcame the main hurdle by finally realizing that I could write cli.stub(:gets).and_return and pass it in the array of commands I wanted to execute (as parameters thanks to the splat * operator), and then pass it the "EXIT" command to finish. So simple, yet so elusive. Many thanks go to this StackOverflow question and answer for pushing me over the line.

Here is the code:

describe "executing instructions from the command line" do
  let(:output) { capture(:stdout) { cli.execute } }

  context "with valid commands" do
    valid_test_data.each do |data|
      let(:expected_output) { data[:output] }
      let(:commands) { StringIO.new(data[:input]).map { |a| a.strip } }

      it "should process the commands and output the results" do
        cli.stub(:gets).and_return(*commands, "EXIT")
        output.should include(expected_output)
      end
    end
  end
  # ...
end
like image 44
Paul Fioravanti Avatar answered Dec 23 '22 04:12

Paul Fioravanti