ymbirtt ymbirtt - 4 months ago 10
Ruby Question

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.

Answer

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_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