ymbirtt
ymbirtt

Reputation: 1666

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.

Upvotes: 3

Views: 1696

Answers (2)

chipairon
chipairon

Reputation: 2061

Using minitest it could be done like I post below. I have added the whole runnable file, so you can test it from the command line with ruby -Ilib:test test_file.rb:

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

class FrabnicateTest < Minitest::Test
  def test_it_works
    mock_file = StringIO.new(%(
      not this line
      foo bar baz
      not hthis line either
    ))
    search_result = nil
    File.stub(:open, nil, mock_file) do
      search_result = frobnicate
    end
    assert_match(/foo bar baz/, search_result)
  end
end

Upvotes: 1

flanger001
flanger001

Reputation: 777

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

Upvotes: 2

Related Questions