Search

Fake It

Andy Lindeman

2 min read

Dec 5, 2011

Fake It

One of the more complicated Ruby/Rails projects we work with at Highgroove has many points where it interacts directly with the filesystem.

Writing tests for an application whose code requires reading from or writing to the filesystem presents challenges, especially if done naively.

While it’s tempting to simply use the real filesystem during unit tests, this presents a few problems:

  • The tests may be brittle, breaking on systems that are not setup just like the initial developer’s local environment.
  • Setting up fixtures on the real filesystem may not be plausible; for instance, if the code interacts with system files (such as in the example coming below!).
  • Test code must be careful to clean up afterwards, even in error cases. Otherwise, the file system could be left polluted, dirtying the developer’s machine and possibly breaking tests on the next run.
  • Tests running in parallel may interact with one another, causing random failures (e.g., on a continuous integration server or with parallel_tests).
  • The filesystem is slow; when attempting to make unit tests as fast as possible, the time to write, sync, and/or read from the filesystem may become significant.

So what’s the solution? Fake the filesystem during unit tests.

More after the break.

Consider the following (contrived) piece of code:

# Reads /etc/passwd and returns an array of arrays
# representing the users on the system.
def users
File.read("/etc/passwd").
split(/\r?\n/).
reject { |line| line =~ /^\s*#/ }.
map { |line| line.split(":") }
end
view raw passwd.rb hosted with ❤ by GitHub

Now, what’s the best way to test it? The tests can’t touch /etc/passwd due to permissions, and even if they could, writing to /etc/passwd would be destructive to the system.

How about mocking File.read?

Assuming we’re using RSpec and mocha for mocking:

describe "#users" do
it "reads /etc/passwd and returns user information" do
File.stubs(:read).returns("andy:*:100:100\nbob:*:101:101")
users.should == [["andy", "*", "100", "100"],
["bob", "*", "101", "101"]]
end
end
view raw wonky_test.rb hosted with ❤ by GitHub

This works, but tightly couples the test code. What if someone comes along and refactors the production code, making use of File.readlines instead of File.read.split?

# Reads /etc/passwd and returns an array of arrays
# representing the users on the system.
def users
File.readlines("/etc/passwd").
reject { |line| line =~ /^\s*#/ }.
map { |line| line.split(":") }
end
view raw new_code.rb hosted with ❤ by GitHub

The code still works, but the test breaks because File.read is stubbed, not File.readlines. In fact, it’s now reading from the real system’s /etc/passwd. The code still works, but the test broke. Ugh!

A better solution is to stub the entire filesystem at a higher level, giving a blank slate each time a new test runs. Nothing is ever actually written to the filesystem.

Enter the fakefs gem.

It’s trivial to get up and running. If using bundler, I recommend adding it to the :test group:

group :test
gem 'fakefs', require: "fakefs/safe"
end
view raw Gemfile hosted with ❤ by GitHub

Next, configure RSpec to include the fakefs helpers to automatically activate and deactivate fakefs whenever a test is tagged with fakefs: true:

# spec_helper.rb
require 'fakefs/spec_helpers'
RSpec.configure do |config|
config.include FakeFS::SpecHelpers, fakefs: true
end
view raw spec_helper.rb hosted with ❤ by GitHub

And finally, write tests that are tagged with fakefs: true. But this time, test code can manipulate any part of the filesystem, knowing it’s completely emphemeral and isolated:

describe "#users", fakefs: true do
def stub_etc_passwd
FileUtils.mkdir("/etc")
File.open("/etc/passwd", "w") do |f|
f.puts("andy:*:100:100")
f.puts("bob:*:101:102")
end
end
it "reads /etc/passwd and returns user information" do
stub_etc_passwd
users.should == [["andy", "*", "100", "100"],
["bob", "*", "101", "101"]]
end
end
view raw new_tests.rb hosted with ❤ by GitHub

Voila! While there is a bit more test code, the test is verifying behavior, not the specific implementation. Do you have any other tips for testing code that interacts with the filesystem?

Josh Justice

Reviewer Big Nerd Ranch

Josh Justice has worked as a developer since 2004 across backend, frontend, and native mobile platforms. Josh values creating maintainable systems via testing, refactoring, and evolutionary design, and mentoring others to do the same. He currently serves as the Web Platform Lead at Big Nerd Ranch.

Speak with a Nerd

Schedule a call today! Our team of Nerds are ready to help

Let's Talk

Related Posts

We are ready to discuss your needs.

Not applicable? Click here to schedule a call.

Stay in Touch WITH Big Nerd Ranch News