8

I want to use RSpec mocks to provide canned input to a block.

Ruby:

class Parser
  attr_accessor :extracted

  def parse(fname)
    File.open(fname).each do |line|
      extracted = line if line =~ /^RCS file: (.*),v$/
    end
  end
end

RSpec:

describe Parser
  before do
    @parser = Parser.new
    @lines = mock("lines")
    @lines.stub!(:each)
    File.stub!(:open).and_return(@lines)
  end

  it "should extract a filename into extracted" do
    linetext = [ "RCS file: hello,v\n", "bla bla bla\n" ]

    # HELP ME HERE ...
    # the :each should be fed with 'linetext'
    @lines.should_receive(:each)

    @parser.should_receive('extracted=')
    @parser.parse("somefile.txt")
  end
end

It's a way to test that the internals of the block work correctly by passing fixtured data into it. But I can't figure out how to do the actual feeding with RSpec mocking mechanism.

update: looks like the problem was not with the linetext, but with the:

@parser.should_receive('extracted=')

it's not the way it's called, replacing it in the ruby code with self.extracted= helps a bit, but feels wrong somehow.

Dave Schweisguth
  • 36,475
  • 10
  • 98
  • 121
Evgeny
  • 6,533
  • 5
  • 58
  • 64
  • One bug is extracted should be the instance variable, @extracted. See my comment for the correct test. – titanous Dec 22 '08 at 01:48

4 Answers4

9

To flesh out the how 'and_yield' works: I don't think 'and_return' is really what you want here. That will set the return value of the File.open block, not the lines yielded to its block. To change the example slightly, say you have this:

Ruby

def parse(fname)
  lines = []
  File.open(fname){ |line| lines << line*2 }
end

Rspec

describe Parser do
  it 'should yield each line' do
    File.stub(:open).and_yield('first').and_yield('second')
    parse('nofile.txt').should eq(['firstfirst','secondsecond'])
  end
end

Will pass. If you replaced that line with an 'and_return' like

File.stub(:open).and_return(['first','second'])

It will fail because the block is being bypassed:

expected: ["firstfirst", "secondsecond"]
got: ["first", "second"]

So bottom line is use 'and_yield' to mock the input to 'each' type blocks. Use 'and_return' to mock the output of those blocks.

sberkley
  • 1,188
  • 1
  • 8
  • 4
4

I don't have a computer with Ruby & RSpec available to check this, but I suspect you need to add a call to and_yields call [1] on the end of the should_receive(:each). However, you might find it simpler not to use mocks in this case e.g. you could return a StringIO instance containing linetext from the File.open stub.

[1] http://rspec.rubyforge.org/rspec/1.1.11/classes/Spec/Mocks/BaseExpectation.src/M000104.html

James Mead
  • 3,472
  • 19
  • 17
  • and_yields does not do what I need, and if it does - then I just cant wrap my head around how the hell it works. – Evgeny Dec 18 '08 at 19:25
  • No, you are right. It looks like the bug I have is actually in the @parser.should_receive('extracted='). it's just not correct ... does not work. – Evgeny Dec 18 '08 at 19:29
  • When I replaced "extracted = " with "self.extracted = " in the ruby code, it started working correctly. Was chasing the wrong bug for the last two days. – Evgeny Dec 18 '08 at 19:47
2

I would go with the idea of stubbing the File.open call

lines = "RCS file: hello,v\n", "bla bla bla\n"
File.stub!(:open).and_return(lines)

This should be good enough to test the code inside the loop.

Jeff Waltzer
  • 532
  • 5
  • 12
1

This should do the trick:

describe Parser
  before do
    @parser = Parser.new
  end

  it "should extract a filename into extracted" do
    linetext = [ "RCS file: hello,v\n", "bla bla bla\n" ]
    File.should_receive(:open).with("somefile.txt").and_return(linetext)
    @parser.parse("somefile.txt")
    @parser.extracted.should == "hello"
  end
end

There are some bugs in the Parser class (it won't pass the test), but that's how I'd write the test.

titanous
  • 3,668
  • 3
  • 27
  • 26