Test Output to Command Line with Rspec

How to write Rspec test for running file from command line?


Don't Use the RSpec Output Matcher

RSpec has a built-in output matcher than can test both where output goes, as well as its contents. However, it's testing where your Ruby output goes, not whether some external application is using standard input or standard error. You're going to have to make some different assumptions about your code.

You can avoid driving yourself nuts by comparing strings rather than testing the underlying shell or your output streams. For example, consider:

RSpec.describe "parse utility output" do
it "prints the right string on standard output" do
expect(`echo hello world`).to start_with("hello world")
end

it "shows nothing on standard output when it prints to stderr" do
expect(`echo foo >&2 > /dev/null`).to be_empty
end
end

Just replace the echo statements with the correct invocation of parse for your system, perhaps by setting PATH directly in your shell, using a utility like direnv, or by modifying ENV["PATH"] in your spec or spec_helper.

As a rule of thumb, RSpec isn't really meant for testing command-line applications. If you want to do that, consider using the Aruba framework to exercise your command-line applications. It's best to use RSpec to test the results of methods or the output of commands, rather than trying to test basic functionality. Of course, your mileage may vary.

How to test stdin for a CLI using rspec

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

RSpec loop testing with console input and output


Refactor to Inject Your Test Inputs into the "Real" Method

This is a common problem for code that isn't written test-first. There are a couple of ways to solve it, but the simplest option without mocking, stubbing, or otherwise invalidating your "real" code is simply to refactor the method itself. For example:

def read test_input: nil
loop do
print "#{RADIUS_QUERY_MSG}\n> "
radius = (test_input || gets).strip
return radius.to_f if valid?(radius)

puts INVALID_MSG
end
end

Now you can simply inject whatever values you want into the optional test_input keyword argument from your RSpec tests to ensure that the input is stripped properly and exhibits whatever other behavior you're testing for.

This avoids all sorts of problems you might experience by trying to write around a difficult-to-test method. Either you provide test input directly to the method, in which case the method uses that, or you don't, in which case it calls #gets just as it normally would.

Remember, the goal isn't to test core methods like #gets. Instead, you should be testing the behavior of your method or object given a particular input or state. If you make your methods testable by allowing dependency injection in your code, or refactoring your class to allow modifying instance variables in your test setup and using those rather than method arguments passed to your methods, you ensure that you are testing your real class or method rather than hacking your way around it.

There are certainly other, more complex ways to do what I did above, but they don't seem warranted for this specific example. The KISS principle definitely applies!

Using RSpec to Test Console Application Interaction

I agree with you that it would be good to have at least one or a few integration tests of the whole app. Those tests shouldn't have mocks; mocks replace dependencies of classes that you want to test in isolation. Here's a simple way to integration-test a command-line app, using irb as an example:

require 'pty'

describe "irb" do
it "evaluates an expression" do
PTY.spawn('irb') do |output, input|
output.readpartial 1024 # read past the prompt
input.puts "1 + 1"
expect(output.readline.chomp).to eq("1 + 1")
expect(output.readline.chomp).to eq("=> 2")
end
end
end

If I had more than one test like this I'd extract an RSpec matcher, but I left the nitty-gritty inline here to keep it simple.

This kind of test is relatively slow, since it runs the app in a separate process with a new Ruby instance, so you only want to write one or a few of these to test that the app works as a whole and then test the details with unit tests of individual classes. What those look like, and whether you need mocks, depends entirely on the internal structure of your app.



Related Topics



Leave a reply



Submit