rubyunit-testingrspecruby-mocha

How can I appropriately mock out a method that returns yield?


It's fairly common in Ruby for methods that take blocks to look like this:

class File
  def open(path, mode)
    perform_some_setup
    yield
  ensure
    do_some_teardown
  end
end

It's also fairly idiomatic for a method to look like this:

def frobnicate
  File.open('/path/to/something', 'r') do |f|
    f.grep(/foo/).first
  end
end

I want to write a spec for this that doesn't hit the filesystem, which ensures it pulls the right word out of the file, something like:

describe 'frobnicate' do
  it 'returns the first line containing the substring foo' do
    File.expects(:open).yields(StringIO.new(<<EOF))
      not this line
      foo bar baz
      not this line either
    EOF
    expect(frobnicate).to match(/foo bar baz/)  
  end
end

The problem here is that, by mocking out the call to File.open, I've also removed its return value, which means that frobnicate will return nil. If I were to add something like File.returns('foo bar baz') to the chain, however, I'd end up with a test that doesn't actually hit any of the code I'm interested in; the contents of the block in frobnicate could do anything and the test would still pass.

How might I appropriately test my frobnicate method without hitting the filesystem? I'm not particularly attached to any particular testing framework, so if your answer is "use this awesome gem that'll do it for you" then I'm OK with that.


Solution

  • It seems like you just need to mock the call to File a little differently. I was getting syntax errors running your code as-is, so I'm not sure what version of RSpec you're on, but if you're on 3.x this will do the job:

    frobnicate_spec.rb

    gem 'rspec', '~> 3.4.0'
    require 'rspec/autorun'
    
    RSpec.configure do |config|
      config.mock_with :rspec
    end
    
    def frobnicate
      File.open('/path/to/something', 'r') do |f|
        f.grep(/foo/).first
      end
    end
    
    RSpec.describe 'frobnicate' do
      it 'returns the first line containing the substring foo' do
        allow(File).to receive(:open).and_call_original
        allow(File).to receive(:open).and_yield StringIO.new <<-EOF
          not this line
          foo bar baz
          not this line either
        EOF
        expect(frobnicate).to match(/foo bar baz/)
      end
    end
    

    Invoke with ruby frobnicate_spec.rb so we can use a specified RSpec version.

    Source: RSpec Mocks expecting messages and yielding responses