How to use yield self within Rspec - ruby

This is a description of how to create a helper method in Rspec taken from the Rspec book (page 149). This example assumes that there is a method called 'set_status' which is triggered when the 'Thing' object is created.
Both sets of code create a new 'Thing' object, set the status, then do 'fancy_stuff'. The first set of code is perfect clear to me. One of the 'it' statements it triggered, which then calls the 'create_thing' method with options. A new 'Thing' object is created and the 'set_status' method is called with the 'options' attribute as the parameter.
The second set of code is similar. One of the 'it' statements is triggered, which then calls the 'given_thing_with' method while passing ':status' hash assignment as a parameter. Within the 'given_thing_with' method the 'yield' is triggered taking the 'Thing.new' as a parameter. This is where I am having trouble. When I try to run this code I get an error of "block given to yield". I understand that whatever attributes that are passed by yield will be returned to the 'thing' in pipe brace from the 'it' statement that called the 'given_thing_with' method. I can get the new
What I don't understand is why the code block is not called in the 'given_thing_with' method after the 'yield' command. In other words, I can't code in that block to run.
Thanks in advance for your help.
The remainder of this question is quoted directly from the Rspec book:
describe Thing do
def create_thing(options)
thing = Thing.new
thing.set_status(options[:status])
thing
end
it "should do something when ok" do
thing = create_thing(:status => 'ok')
thing.do_fancy_stuff(1, true, :move => 'left', :obstacles => nil)
...
end
it "should do something else when not so good" do
thing = create_thing(:status => 'not so good')
thing.do_fancy_stuff(1, true, :move => 'left', :obstacles => nil)
...
end
end
One idiom you can apply to clean this up even more is to yield self from initializers in your objects. Assuming that Thing's initialize() method does this and set_status() does as well, you can write the previous like this:
describe Thing do
def given_thing_with(options)
yield Thing.new do |thing|
thing.set_status(options[:status])
end
end
it "should do something when ok" do
given_thing_with(:status => 'ok') do |thing|
thing.do_fancy_stuff(1, true, :move => 'left', :obstacles => nil)
...
end
end
it "should do something else when not so good" do
given_thing_with(:status => 'not so good') do |thing|
thing.do_fancy_stuff(1, true, :move => 'left', :obstacles => nil)
...
end
end
end

The example in the book is a bit confusing because the implementation of Thing is not shown. To make this work you need to write Thing like so:
class Thing
def initialize
yield self
end
end
When given_thing_with is called it yields a new Thing, which will yield itself when it is constructed. This means that when the inner code block (the one containing thing.set_status) is executed it will have a reference to he newly built Thing.

There are 2 issues with the code from book.
1. Setting up the initializer to yield itself
When the Thing object is created, it needs an initializer and need yield itself.
class Thing
def initialize
yield self
end
end
However, this alone will still causes an error, at least on my system, which is Ruby 1.9.3. Specifically, the error is 'block given to yield (SyntaxError)'. This doesn't make much sense, since that is what we want it to do. Regarless, that is the error I get.
2. Fixing the 'block given to yield' error
This is not as obvious and has something to do with either Ruby or the 'yield' statement, but creating a block using 'do...end' as was written in the book and is shown below causes the error.
yield Thing.new do |thing|
thing.set_status(options[:status])
end
Fixing this error is simlpy a matter of creating the block using braces, '{...}', as is shown below.
yield Thing.new { |thing|
thing.set_status(options[:status])
}
This is not good form for multiline Ruby code, but it works.
Extra. How the series of yields works to set the parameters of the 'Thing' object
The problem is already fixed, but this explains how it works.
the "caller block" calls 'given_thing_with' method with a parameter
that method yields back to the "caller block" a new "Thing" and a block (I'll call it the "yield block")
to execute the "yield block", the Thing class needs the initialization and 'yield self', otherwise the 'set_status' method will never be run because the block will be ignored
the new "Thing" is already in the "caller block" and has it's status set and now the relevant method is executed

Related

Testing gets in rspec (user input)

My class has this #run method that so far is just this, to test the testing:
def run
puts "Enter 'class' to create a new class."
input = $stdin.gets.chomp
binding.pry
And in the tests so far I've got
allow($stdin).to receive(:gets).and_return 'class'
cli.run
Doing it this way I am able to see, in the pry session, that input has been set to 'class', as intended.
Is there a way to do with without adding $stdin to my call to gets in my method itself? i.e., input = gets.chomp
I've tried allow(cli.run).to receive(:gets).and_return 'class'
But then in the pry session, input is equal to the first line of the spec file!
You can avoid this as such:
def run
puts "Enter 'class' to create a new class."
input = gets.chomp
end
describe 'gets' do
it 'belongs to Kernel' do
allow_any_instance_of(Kernel).to receive(:gets).and_return('class')
expect(run).to eq('class')
end
end
The method gets actually belongs to the Kernel module. (method(:gets).owner == Kernel). Since Kernel is included in Object and almost all ruby objects inherit from Object this will work.
Now if run is an instance method scoped in a Class I would recommend scoping the stubbing a bit more such that:
class Test
def run
puts "Enter 'class' to create a new class."
input = gets.chomp
end
end
describe 'gets' do
it 'can be stubbed lower than that' do
allow_any_instance_of(Test).to receive(:gets).and_return('class')
expect(Test.new.run).to eq('class')
end
# or even
it 'or even lower than that' do
cli = Test.new
allow(cli).to receive(:gets).and_return('class')
expect(cli.run).to eq('class')
end
end
Example

How to prevent problems with `return` from block when using Ruby `yield`

As every Ruby programmer eventually discovers, calling blocks or procs that contain return statements can be dangerous as this might exit your current context:
def some_method(&_block)
puts 1
yield
# The following line will never be executed in this example
# as the yield is actually a `yield-and-return`.
puts 3
end
def test
some_method do
puts 2
return
end
end
test
# This prints "1\n2\n" instead of "1\n2\n3\n"
In cases you want to be absolutely sure some of your code runs after you called a block or proc, you can use a begin ... ensure construct. But since ensure is also called if there is an exception during yield, it requires a little more work.
I've created a tiny module that deals with this problem in two different ways:
Using safe_yield, it is detected whether the yielded block or proc actually returns using the return keyword. If so, it raises an exception.
unknown_block = proc do
return
end
ReturnSafeYield.safe_yield(unknown_block)
# => Raises a UnexpectedReturnException exception
Using call_then_yield, you can call a block and then make sure that a second block is executed, even if the first block contains a return statement.
unknown_block = proc do
return
end
ReturnSafeYield.call_then_yield(unknown_block) do
# => This line is called even though the above block contains a `return`.
end
I'm considering to create a quick Gem out of this, or is there any built-in solution to prevent quick return from the nested block which I missed?
There is a built-in solution to detect whether a block contains a return statement.
You can use RubyVM::InstructionSequence.disasm to disassemble the block passed in by the user, then search it for throw 1, which represents a return statement.
Here's a sample implementation:
def safe_yield(&block)
if RubyVM::InstructionSequence.disasm(block) =~ /^\d+ throw +1$/
raise LocalJumpError
end
block.call
end
Here's how you might incorporate it into your library:
def library_method(&block)
safe_yield(&block)
puts "library_method succeeded"
rescue LocalJumpError
puts "library_method encountered illegal return but resumed execution"
end
And here's the user experience for a well-behaved and a misbehaving user:
def nice_user_method
library_method { 1 + 1 }
end
nice_user_method
# library_method succeeded
def naughty_user_method
library_method { return false if rand > 0.5 }
end
naughty_user_method
# library_method encountered illegal return but resumed execution
Commentary:
Using raise LocalJumpError/rescue LocalJumpError gets around the issues you encountered when using a blanket ensure.
I chose LocalJumpError because it seems relevant, and because (I think!) there is no possible Ruby code that would result in LocalJumpError being raised "naturally" in this context. If that turns out to be false, you can easily substitute your own new exception class.

Does should_receive do something I don't expect?

Consider the following two trivial models:
class Iq
def score
#Some Irrelevant Code
end
end
class Person
def iq_score
Iq.new(self).score #error here
end
end
And the following Rspec test:
describe "#iq_score" do
let(:person) { Person.new }
it "creates an instance of Iq with the person" do
Iq.should_receive(:new).with(person)
Iq.any_instance.stub(:score).and_return(100.0)
person.iq_score
end
end
When I run this test (or, rather, an analogous one), it appears the stub has not worked:
Failure/Error: person.iq_score
NoMethodError:
undefined method `iq_score' for nil:NilClass
The failure, as you might guess, is on the line marked "error here" above. When the should_receive line is commented out, this error disappears. What's going on?
Since RSpec has extended stubber functionality, now following way is correct:
Iq.should_receive(:new).with(person).and_call_original
It will (1) check expectation (2) return control to original function, not just return nil.
You're stubbing away the initializer:
Iq.should_receive(:new).with(person)
returns nil, so Iq.new is nil. To fix, just do this:
Iq.should_receive(:new).with(person).and_return(mock('iq', :iq_score => 34))
person.iq_score.should == 34 // assert it is really the mock you get

Why is the stubbed method not called?

It seems I understood something wrong. I have a class
module Spree
class OmnikassaPaymentResponse
#...
# Finds a payment with provided parameters trough ActiveRecord.
def payment(state = :processing)
Spree::Payment.find(:first, :conditions => { :amount => #amount, :order_id => #order_id, :state => state } ) || raise(ActiveRecord::RecordNotFound)
end
end
end
Which is specced in Rspec:
describe "#payment" do
it 'should try to find a Spree::Payment' do
Spree::Payment.any_instance.stub(:find).and_return(Spree::Payment.new)
Spree::Payment.any_instance.should_receive(:find)
Spree::OmnikassaPaymentResponse.new(#seal, #data).payment
end
end
This, however, always throws ActiveRecord::RecordNotFound. I expected any_instance.stub(:find).and_return() to make sure that whenever, wherever I call a #find on whatever instance I happen to have of Spree::Payment, it returns something.
In other words: I would expect the stub.and_return would avoid getting to || raise(ActiveRecord::RecordNotFound). But it does not.
Is my assumption wrong, my code? Something else?
In your case find is not an instance method, but a class method of Spree::Payment. That means you should stub it directly without any_instance like that:
Spree::Payment.stub(:find).and_return(Spree::Payment.new)

RSpec: How do you implicitly filter tests based on the outcome of a previous test?

I'm iterating through a tree control on a webpage. Clicking on some nodes in the tree will change the content in FRAME_C, clicking on others will not. How do I filter a test to only run when the content has changed? Here's what I'm trying:
def viewDifferent?
if $prvView != $curView
return true
else
return false
end
end
...
describe "Exercising View" do
it "clicks a node in the tree control" do
$prvView = $b.frame( :id, 'FRAME_C').document.body.innertext
Timeout.timeout(50) do
spn.fire_event('onmouseup')
end
$curView = $b.frame( :id, 'FRAME_C').document.body.innertext
end
it "Runs only if the view is different", :if => viewDifferent? do
puts "Doing some stuff."
end
end
My problem is that RSpec is evaluating the filter for all of my tests before executing any of them. In the above example viewDifferent? will always (and does) return false since the two global variables have yet to be set by the previous test.
Is there a way to do what I'm asking? I've been trying to figure this out for days.
A test should always run. It should setup the state it requires to execute the code path you expect. It seems to me that executing tests conditionally based on the outcome of other tests totally breaks the spirits of the tests.
You should already know the previous view and the current view are different, and if are not what you expect you have a failure.
Every test should have a very specific path through your code you expect it to execute, and you should fail if it doesn't. There isn't a way to do what you want because you shouldn't do it that way.
I'm not familiar w/ rspec, but have you tried using a Proc? For example...
it "Runs only if the view is different", :if => lambda { viewDifferent? } do
puts "Doing some stuff."
end
A symbol as shorthand may even work...
it "Runs only if the view is different", :if => :viewDifferent? do
puts "Doing some stuff."
end
As you currently have it, it's calling the viewDifferent? method as soon as the test is declared. What you really want is to pass a Proc so that it gets called when the test is run.

Resources