Brand
Brand

Reputation: 1701

How do you stub a file.read that happens inside a File.open block?

How do I stub a file.read call so that it returns what I want it to? The following does not work:

def write_something
  File.open('file.txt') do |f|
    return contents = f.read
  end
end
# rspec
describe 'stub .read' do
  it 'should work' do
    File.stub(:read) { 'stubbed read' }
    write_something.should == 'stubbed read'
  end
end

It looks like the stub is being applied to the File class and not the file instance inside my block. So File.read returns stubbed read as expected. But when I run my spec it fails.

Upvotes: 9

Views: 9945

Answers (3)

Andrew Marshall
Andrew Marshall

Reputation: 96964

I should note that File.open is just one part of Ruby’s very large I/O API, and so your test is likely to be very strongly coupled to your implementation, and unlikely to survive much refactoring. Further, one must be careful with “global” mocking (i.e. of a constant or all instances) as it can unintentionally mock usages elsewhere, causing confusing errors and failures.

Instead of mocking, consider either creating an actual file on disk (using Tempfile) or using a broader I/O mocking library (e.g. FakeFS).

If you still wish to use mocking you can somewhat safely stub File.open to yield a double (and only when called with the correct argument):

file = instance_double(File, read: 'stubbed read')
allow(File).to receive(:open).and_call_original
allow(File).to receive(:open).with('file.txt') { |&block| block.call(file) }

or, somewhat dangerously, stub all instances:

allow_any_instance_of(File).to receive(:read).and_return('stubbed read')

Upvotes: 13

Prashant Vithani
Prashant Vithani

Reputation: 51

This is how I'd do it

    describe 'write_something' do
      it 'should work' do  
        file_double = instance_double('File')
        expect(File).to receive(:open).with('file.txt').and_yield(file_double)
        expect(file_double).to receive(:read).and_return('file content')
        content = write_something
        expect(content).to eq('file content')
      end
    end  

Upvotes: 0

Eduardo Santana
Eduardo Santana

Reputation: 6110

The main point is to make File.open to return an object that will respond to read with the content you want, here's the code:

    it "how to mock ruby File.open with rspec 3.4" do
      filename = 'somefile.txt'
      content = "this would be the content of the file"
      # this is how to mock File.open:
      allow(File).to receive(:open).with(filename, 'r').and_yield( StringIO.new(content) )
      # you can have more then one of this allow

      # simple test to see that StringIO responds to read()
      expect(StringIO.new(content).read).to eq(content)

      result = ""
      File.open('somefile.txt', 'r') { |f| result = f.read }
      expect(result).to eq(content)
    end

Upvotes: 1

Related Questions