StringIO Test Fake
Video transcript & code
In Episode #398, we got our first proper introduction to the StringIO standard library. But we only saw one example usage for it. Today I want to demonstrate another area where I find this library to be useful: in tests.
Let's say we have a method called
copy_with_backup. Its job is to copy an input file to both an output file and a backup file.
Some other method is responsible for finding and opening the appropriate files. This method is only responsible for performing the copies. In order to do the copying efficiently, it uses
IO.copy_stream, which we first encountered in Episode #74.
def copy_with_backup(input, output, backup) IO.copy_stream(input, output) input.rewind IO.copy_stream(input, backup) end
Now let's say we want to test this method.
We could do it by creating a test input file, and then opening up two test output files and passing their
IO objects in. But this would be a lot of test code. We'd have to be very careful to clean up the files afterwards. And working with real files is likely to slow our test suite down if we use that technique for more than a few tests.
require "rspec/autorun" RSpec.describe "copy_with_backup" do it "copies input to output and backup streams" do end end
But what else can we use? Those
copy_stream calls are expecting either real
IO objects, or something which behaves exactly like them. Not to mention whatever we pass in as
input must respond to the
If you've watched Episode #398, you probably know what's coming next. Ruby provides the
StringIO class to act as a stand-in for an IO object, one that backed by a simple Ruby string.
Let's apply what we know about
StringIO to testing the
We'll create three
StringIO objects: one to stand-in for the input file, complete with some sample text.
A blank one to stand-in for the output file.
And another blank one to stand in for the backup.
Then we'll exercise the method under test, using these fake files objects.
Finally, we'll verify that the string contents of the output and backup
StringIO objects matches the contents of the input.
The result is a passing test.
require "rspec/autorun" require "./copy_with_backup" require "stringio" RSpec.describe "copy_with_backup" do it "copies input to output and backup streams" do input = StringIO.new("SOURCE TEXT") output = StringIO.new backup = StringIO.new copy_with_backup(input, output, backup) expect(output.string).to eq("SOURCE TEXT") expect(backup.string).to eq("SOURCE TEXT") end end # >> . # >> # >> Finished in 0.00223 seconds (files took 0.13607 seconds to load) # >> 1 example, 0 failures # >>
This code runs fast, and doesn't require any special cleanup.
Now, you might be thinking that we're using these
StringIO objects as "mock" or "stub" objects. But I want to draw a distinction here.
You might remember from Episode #287 that the cardinal rule of mock objects is: only mock what you own. The
IO family of objects is a great example of something we don't own. The Ruby implementation owns those objects.
So rather than trying to mock IO objects, we've instead replaced one thing that the system provides, with a different thing that the system also provides.
In this context,
StringIO functions effectively as a test fake: we can use it to stand in for file objects, without knowing or caring what parts of a file object
copy_stream makes use of. We just know that the system promises that
StringIO is interchangeable with other
IO objects, and that's good enough for us.