How to Test Ruby IO

Receiving input from a user, doing something with that input, and then displaying some output is core to software development. This input and output is reffered to as IO.

In TDD, we should test the behavior of all the code we write, but testing IO can be challenging because we don’t want to be prompted for input or have miscellaneous text printed in the terminal while running our tests. I’m going to walk through a scenario where we have a UI class (i.e., user interface) in Ruby that handles all of the IO and explain how to test it. We’re also going to look at the IO class and its STDIN and STDOUT constants.

Now, normally I would write the test first, but I think it’s a good idea to clarify what I mean by the UI class. So here it is:

class UI
     def give(output_message)
          puts output_message
     end

     def receive
          gets.chomp
     end
end

It should be pretty clear what’s happening here. If you call the give method on the UI class with some message, the message will be displayed to the user on the command line. If you call the receive method on the UI class, the user will be prompted to enter some info and a string with that info will be returned.

At least at first, there is not an obvious solution for testing the behavior of this really simple UI class without actually calling puts and gets and using the terminal.

In order to move forward with a good solution, we need to learn about IO, StringIO, STDIN, and STDOUT.

IO, Kernel, Constant, and Global Variables

There are three constants in Ruby that are automatically defined as objects in the IO class: STDIN, STDOUT, and STDERR. These refer to the program’s standard input, standard output, and standard error streams, respectively. You can think of each one of these as a file that is already created and opened for you. You can see that IO#fileno returns the associated file descriptor as an Integer.

STDIN.fileno  #=> 0
STDOUT.fileno #=> 1
STDERR.fileno #=> 2

At the same time, three global variables are defined automatically in the Kernel module: $stdin, $stdout, and $stderr. The Kernel module is included by the Object class, so its methods are available in every Ruby object. Therefore, the global variables $stdin, $stdout, and $stderr are available in every Ruby object. Unless they are changed, these global variables hold the value of their corresponding constant.

We can verify these two variables are actually pointing at the same object by looking at their object id’s:

$stdin.object_id   # =>  70173541627420
STDIN.object_id    # =>  70173541627420
$stdout.object_id  # =>  70173541627360
STDOUT.object_id   # =>  70173541627360
$stderr.object_id  # =>  70173541627300
STDERR.object_id   # =>  70173541627300

Puts & Gets

Common input and ouput methods in Ruby such as gets, print, puts, and gets, are actually $stdin.gets, $stdout.print, and $stdout.puts. You don’t need to include the global variables $stdin and $stdout to call the method so you usually don’t see them. This is evidenced here:

puts "hello world!"         # => "hello world!"
$stdout.puts "hello world!" # => "hello world!"

In this example, “hello world!” will print to whatever standard out stream $stdout is directed to. If you haven’t made any changes or redirects then $stdout automatically refers to the constant STDOUT, which prints to your terminal. Similary, the automatic standard input stream, or STDIN, prompts the user for information from the terminal which is what happens when you call ‘gets’ or $stdin.gets.

So why are there two variables referring to these standard IO streams? The reason for the existence of these global variables in addition to their constants is that by assigning another IO object to one of the global variables, we can temporarily redirect the IO elsewhere. Then we can always restore the original behavior by setting the global variables back to equal the corresponding constant.

And guess what… this is how we can test IO without actually read and writing from the terminal!

How to Test IO

Instead of using just puts and gets, we need to add the global variable in front of the method so that we can temporarily redirect the standard input and output streams. Here’s the code we will end up with:

class UI
     def give(output_message, stdout: $stdout)
         stdout.puts output_message
     end

     def receive(stdin: $stdin)
          stdin.gets.chomp
     end
end

You can see that stdout and stdin are automatically set to equal the global variables which refer to STDIN and STDOUT. However, if we want to we can pass them another value, changing the input and output streams for puts and gets. And while we’re testing that’s exactly what we’ll do.

Here’s the rspec tests we can use to test the above code:

require 'ui.rb'
describe "UI" do
     before(:each) do
          @ui = UI.new
          @input = StringIO.new("test info\n")
          @output = StringIO.new
          @input_received = @ui.receive(@input)
          @ui.give("test message", @output)
     end
     it "should output a message for the user" do
          @output.string.should =~ /test message/
     end
     it "should return the user's input" do
          @input_received.should == "test info"
     end
end

StringIO is a pseudo IO. It basically makes a string look like an IO object. The StringIO class has both read and write methods, so it can be passed to parts of your code that were designed to read or write. It’s perfect for testing because we can make a string look like a file which we can read from or write to.

In our case, we pass a StringIO to the gets.chomp method in place of the global variable $stdin and we write to a StringIO file by passing it to puts in place of the global variable $stdout.

There are other ways to test IO, but I’ve found this to be the simplest. I also really enjoyed getting to the bottom of the global variables in Kernel vs. the constants in the Object class. Hopefully this clarifies it for you too!

References:
http://rubymonk.com/learning/books/1-ruby-primer/chapters/42-introduction-to-i-o/lessons/89-streams#solution4164
http://readruby.io/io
http://www.ruby-doc.org/core-2.1.2/Kernel.html
http://www.ruby-doc.org/core-2.1.2/Object.html

Leave a Reply

Your email address will not be published. Required fields are marked *