Temp files in RSpec

I’ve been hard at work at Chat Stew, the awesome upgrade to Pidgin2Adium. My tests involve creating a lot of temporary files to test various inputs. I had a hard time finding a solution, and figured other people might be having the same troubles I am.

I looked at FakeFS and Construct (test-construct), but neither one does exactly what I want. FakeFS has good RSpec integration, but I want a real filesystem – I just need it to reliably delete files after I’m done with them. Construct is not easy to integrate with RSpec – they have Test::Unit helpers, but for RSpec I tried this (and it failed):
in spec_helper.rb:

RSpec.configure do |spec|
    spec.include Construct::Helpers
end

and the test:

within_construct do |construct|
  construct.file("log.html", "test content")
  it { should do_things_correctly } # etc
end

and that didn’t work. I also thought of Aruba, but that’s Cucumber-only.

So how can I get temp files that:

  • are guaranteed to be deleted when I don’t need them anymore, and
  • preferably have an easy way to specify their content?

As to the first part, Factory Girl has a wonderful bit of code in its (her?) spec: in_directory_with_files(*files). It is run in a context block, and it chdir’s to a tmp directory, creates an empty file for each file passed to the method, runs the specs, and removes the files afterward. It uses before and after blocks to handle creating the files and cleaning up. However, I didn’t want to have to do def self.in_directory_with_files(*files) for every context block, so I factored it out to a module and used a trick from FakeFS (the self.included bit below) to make it work when included in a describe block. I only use one temp file at a time, so my solution also dynamically creates a method called content_for_file which I use to specify that file’s content in it blocks.

Without further ado:

module UsesTempFiles
  def self.included(example_group)
    example_group.extend(self)
  end

  def in_directory_with_file(file)
    before do
      @pwd = Dir.pwd
      @tmp_dir = File.join(File.dirname(__FILE__), 'tmp')
      FileUtils.mkdir_p(@tmp_dir)
      Dir.chdir(@tmp_dir)

      FileUtils.mkdir_p(File.dirname(file))
      FileUtils.touch(file)
    end

    define_method(:content_for_file) do |content|
      f = File.new(File.join(@tmp_dir, file), 'a+')
      f.write(content)
      f.flush # VERY IMPORTANT
      f.close
    end

    after do
      Dir.chdir(@pwd)
      FileUtils.rm_rf(@tmp_dir)
    end
  end
end

Here it is in action:

describe ChatStew::ProgramProtocol do
  # "Percolates" down, so including it in an outer describe block gives the method to all blocks inside it
  include UsesTempFiles

  context "with non-empty file" do
    in_directory_with_file('non_empty_log')

    # You can also put the content_for_file call in an "it" block instead
    before do
      content_for_file("blah")
    end

    it "does things correctly"
  end
end