Search code examples
rubyrspecstringio

Is there such a thing as opening a StringIO for writing?


I am trying to use RSpec to test a class that writes to a file. But I want the tests to be fast, so instead of using a real file and write to disk I want to use StringIO in tests and write to memory.

In a very simplified way, let's say I have this test:

RSpec.describe Writer do
  it 'replaces the contents of the file' do
    file = StringIO.new('foo')
    writer = described_class.new(file)
    one_contact = [{ 'name' => 'name', 'address' => 'address' }]

    writer.write(one_contact)

    expect(file.string).to eq('[{"name":"name1","address":"address1"}]')
  end
end

and let's say the writer class is like this:

require 'json'

class Writer
  def initialize(file)
    @file = file
  end

  def write(contacts)
    file.truncate(0)
    file.write(contacts.to_json)
    file.flush
  end

  private

  attr_reader :file
end

when I run the tests through Rspec I get the error:

 Failure/Error: file.truncate(0)

 IOError:
   not opened for writing

If I change truncate for something else then I get this error in the line that calls write on file.

However, if I do it in pry, it works:

$ pry
[1] pry(main)> require_relative 'lib/db/writer'
=> true
[2] pry(main)> file = StringIO.new('foo')
=> #<StringIO:0x0000563c99220f70>
[3] pry(main)> writer = Writer.new(file)
=> #<Writer:0x0000563c98ff8950 @file=#<StringIO:0x0000563c99220f70>>
[4] pry(main)> one_contact = [{ 'name' => 'name', 'address' => 'address' }]
=> [{"name"=>"name", "address"=>"address"}]
[5] pry(main)> writer.write(one_contact)
=> #<StringIO:0x0000563c99220f70>
[6] pry(main)> file.string
=> "[{\"name\":\"name\",\"address\":\"address\"}]"

If I do it in a normal ruby file and run it with ruby, for example:

require_relative 'lib/db/writer'

file = StringIO.new('foo')
writer = Writer.new(file)

one_contact = [{ 'name' => 'name1', 'address' => 'address1' }]
writer.write(one_contact)
puts file.string

It works as well.

This is a super simple Ruby app, no dependencies or frameworks, this is what the Gemfile looks like:

source 'https://rubygems.org'
ruby '2.5.1'

gem 'sqlite3'

group :test do
  gem 'coveralls', require: false
  gem 'pry'
  gem 'rake'
  gem 'rspec'
  gem 'rubocop', require: false
end

Actual gem versions:

  • sqlite3 (1.3.13)
  • coveralls (0.8.22)
  • pry (0.11.3)
  • rake (12.3.1)
  • rspec (3.7.0)
  • rubocop (0.58.1)

How can I open a StringIO for writing? I want to keep writing to memory rather than disk on tests.


Solution

  • Here's your problem:

    # frozen_string_literal: true
    

    Your string contents is frozen and can't be modified. StringIO expresses it with the abovementioned IOError.