Twitter GitHub Facebook Instagram dirv.me

Daniel Irvine on building software

A better StringIO

30 May 2014

The Ruby StringIO class can be used as a fake during testing of normal IO functions, such as when calling IO#gets or IO#puts. Unfortunately, it’s not a very sophisticated fake and won’t work for IO.console:

def prompt_for_answer(io)
  io.puts What's your answer, then?
  io.gets.chomp
end

it should read do
  io = StringIO.new(test\n)
  expect(prompt_for_answer).to eq test 
end

On the console (passing in the Kernel class object as the io argument), the method prompt_for_answer will work as expected. However, the RSpec test will fail because gets reads from whatever position in the string is returned after the call to puts. In this particular class, the call to gets will return nil.

One way to fix this is to add a call to IO#rewind:

def prompt_for_answer(io)
  io.puts What's your answer, then?
  io.rewind    #  this code only exists to satisfy the test
  io.gets.chomp
end

prompt_for_answers(IO.console) # must now pass in IO.console, not Kernel

This smells, because we’ve had to change our production code to make our test pass. Removing the code would still give a working application, but the test would break. We’ve lost our “minimal” program state.

A better solution is build your own IO fake, either subclassing StringIO if you need to use a multiple of methods, or if you only need gets, puts and string then use the class below:

class SimplifiedStringIO

  def initialize(input_str = '')
    @reader = StringIO.new(input_str)
    @writer = StringIO.new
  end
  
  def puts(str)
    @writer.puts(str)
  end

  def read
    @writer.read
  end

  def string
    @writer.string
  end

  def gets
    @reader.gets
  end
end

Your mileage may vary.

About the author

Daniel Irvine is a software craftsman at 8th Light, based in London. These days he prefers to code in Clojure and Ruby, despite having been a C++ and C# developer for the majority of his career.

For a longer bio please see danielirvine.com. To contact Daniel, send a tweet to @d_ir or use the comments section below.

Twitter GitHub Facebook Instagram dirv.me