Rspec: expect vs expect with block - what's the difference? - ruby

Just learning rspec syntax and I noticed that this code works:
context "given a bad list of players" do
let(:bad_players) { {} }
it "fails to create given a bad player list" do
expect{ Team.new("Random", bad_players) }.to raise_error
end
end
But this code doesn't:
context "given a bad list of players" do
let(:bad_players) { {} }
it "fails to create given a bad player list" do
expect( Team.new("Random", bad_players) ).to raise_error
end
end
It gives me this error:
Team given a bad list of players fails to create given a bad player list
Failure/Error: expect( Team.new("Random", bad_players) ).to raise_error
Exception:
Exception
# ./lib/team.rb:6:in `initialize'
# ./spec/team_spec.rb:23:in `new'
# ./spec/team_spec.rb:23:in `block (3 levels) in <top (required)>'
My question is:
Why does this happen?
What is the difference between the former and later example exactly in ruby?
I am also looking for rules on when to use one over the other
One more example of the same but inverse results, where this code works:
it "has a list of players" do
expect(Team.new("Random").players).to be_kind_of Array
end
But this code fails
it "has a list of players" do
expect{ Team.new("Random").players }.to be_kind_of Array
end
Error I get in this case is:
Failure/Error: expect{ Team.new("Random").players }.to be_kind_of Array
expected #<Proc:0x007fbbbab29580#/Users/amiterandole/Documents/current/ruby_sandbox/tdd-ruby/spec/team_spec.rb:9> to be a kind of Array
# ./spec/team_spec.rb:9:in `block (2 levels) in <top (required)>'
The class I am testing looks like this:
class Team
attr_reader :name, :players
def initialize(name, players = [])
raise Exception unless players.is_a? Array
#name = name
#players = players
end
end

As has been mentioned:
expect(4).to eq(4)
This is specifically testing the value that you've sent in as the parameter to the method. When you're trying to test for raised errors when you do the same thing:
expect(raise "fail!").to raise_error
Your argument is evaluated immediately and that exception will be thrown and your test will blow up right there.
However, when you use a block (and this is basic ruby), the block contents isn't executed immediately - it's execution is determined by the method you're calling (in this case, the expect method handles when to execute your block):
expect{raise "fail!"}.to raise_error
We can look at an example method that might handle this behavior:
def expect(val=nil)
if block_given?
begin
yield
rescue
puts "Your block raised an error!"
end
else
puts "The value under test is #{val}"
end
end
You can see here that it's the expect method that is manually rescuing your error so that it can test whether or not errors are raised, etc. yield is a ruby method's way of executing whatever block was passed to the method.

In the first case, when you pass a block to expect, the execution of the block doesn't occur until it's time to evaluate the result, at which point the RSpec code can catch any error that are raised and check it against the expectation.
In the second case, the error is raised when the argument to expect is evaluated, so the expect code has no chance to get involved.
As for rules, you pass a block or a Proc if you're trying to test behavior (e.g. raising errors, changing some value). Otherwise, you pass a "conventional" argument, in which case the value of that argument is what is tested.

Related

Rspec: undefined method `StandardError' for EmeraldComponent:Module

In my gem I have the following module:
module EmeraldComponent
def self.create(full_name)
raise StandardError('Base directory for components is missing.') if base_directory_missing?
raise StandardError('An Emerald Component must have a name.') if full_name.empty?
raise StandardError('An Emerald Component must have a namespace.') if simple_name?(full_name)
write_component(full_name)
true
end
def self.write_component(full_name)
## To be implemented
end
def self.simple_name?(full_name)
vet = full_name.split('.')
vet.length == 1
end
def self.base_directory_missing?
not (File.exist?(EmeraldComponent::BASE_DIRECTORY) && File.directory?(EmeraldComponent::BASE_DIRECTORY))
end
end
And among my Rspec tests for this module I have these:
context 'create' do
it 'raises an error if the base directory for components is missing' do
expect {
EmeraldComponent.create('test.component.Name')
}.to raise_error(StandardError)
end
it 'raises an error if it receives an empty string as component name' do
expect {
EmeraldComponent.create('')
}.to raise_error(StandardError)
end
it 'raises an error if it receives a non-namespaced component name' do
expect {
EmeraldComponent.create('test')
}.to raise_error(StandardError)
end
it 'returns true if it receives a non-empty and namespaced component name' do
expect(EmeraldComponent.create('test.component.Name')).to be true
end
It happens that when I run the test all of them are passing, except for the first. This gives me the following error.
1) EmeraldComponent Methods create returns true if it receives a non-empty and namespaced component name
Failure/Error: raise StandardError('Base directory for components is missing.') if base_directory_missing?
NoMethodError:
undefined method `StandardError' for EmeraldComponent:Module
# ./lib/EmeraldComponent.rb:10:in `create'
# ./spec/EmeraldComponent_spec.rb:48:in `block (4 levels) in <top (required)>'
As you may see, it is saying that StandardError is undefined for EmeraldComponent:Module.
But StandardError does not belong to EmeraldComponent:Module!
And besides, this same StandardError is working fine for the other tests!.
I've been fighting this error for a while and then decided to post here. Any suggestions?
You should be doing StandardError.new in place or StandardError in your create method
def self.create(full_name)
raise StandardError.new('Base directory for components is missing.') if base_directory_missing?
raise StandardError.new('An Emerald Component must have a name.') if full_name.empty?
raise StandardError.new('An Emerald Component must have a namespace.') if simple_name?(full_name)
write_component(full_name)
true
end
#victorCui is correct.
Instead of raising StandardError.new(), the recommended approach is to write:
raise StandardError, "message"
See https://github.com/rubocop-hq/ruby-style-guide#exception-class-messages

Rspec 3 vs Rspec 2 matchers

Learning how to Rspec 3. I have a question on the matchers. The tutorial i am following is based on Rspec 2.
describe Team do
it "has a name" do
#Team.new("Random name").should respond_to :name
expect { Team.new("Random name") }.to be(:name)
end
it "has a list of players" do
#Team.new("Random name").players.should be_kind_of Array
expect { Team.new("Random name").players }.to be_kind_of(Array)
end
end
Why is the code causing an error while the one i commented out passing with depreciation warning.
Error
Failures:
1) Team has a name
Failure/Error: expect { Team.new("Random name") }.to be(:name)
You must pass an argument rather than a block to use the provided matcher (equal :name), or the matcher must implement `supports_block_expectations?`.
# ./spec/team_spec.rb:7:in `block (2 levels) in <top (required)>'
2) Team has a list of players
Failure/Error: expect { Team.new("Random name").players }.to be_kind_of(Array)
You must pass an argument rather than a block to use the provided matcher (be a kind of Array), or the matcher must implement `supports_block_expectations?`.
# ./spec/team_spec.rb:13:in `block (2 levels) in <top (required)>'
You should use normal brackets for those tests:
expect(Team.new("Random name")).to eq :name
When you use curly brackets, you are passing a block of code. For rspec3 it means that you will put some expectations about the execution of this block rather than on the result of execution, so for example
expect { raise 'hello' }.to raise_error
EDIT:
Note however that this test will fail, as Team.new returns an object, not a symbol. You can modify your test so it passes:
expect(Team.new("Random name")).to respond_to :name
# or
expect(Team.new("Random name").name).to eq "Random name"

ArgumentError in rspec

I want to write some tree data structure in ruby. The class file:
class Tree
attr_accessor :node, :left, :right
def initialize(node, left=nil, right=nil)
self.node=node
self.left=left
self.right=right
end
end
The rspec file:
require 'init.rb'
describe Tree do
it "should be created" do
t2=Tree.new(2)
t1=Tree.new(1)
t=Tree.new(3,t1,t2)
t.should_not be nil
t.left.node should eql 1
t.right.node should eql 2
end
end
Rspec keeps complaining:
1) Tree should be created
Failure/Error: t.left.node should eql 1
ArgumentError:
wrong number of arguments (0 for 1)
# ./app/tree.rb:3:in `initialize'
# ./spec/tree_spec.rb:9:in `block (2 levels) in <top (required)>'
Why?? I move the spec code into the class file and it works out. What is wrong?
Believe it or not, the problem is two missing dots in your rspec. These lines:
t.left.node should eql 1
t.right.node should eql 2
should be this:
t.left.node.should eql 1
t.right.node.should eql 2
Insert that period before should, and your spec should pass.
Here's what's going on. The should method works on any value, but if you call it bare, like this:
should == "hello"
it will operate on the subject of your test. What's the subject? Well, you can set the subject to whatever you want using the subject method, but if you don't, rspec will assume the subject is an instance of whatever class is being described. It sees this at the top of your spec:
describe Tree
and tries to create a subject like this:
Tree.new
which blows up, since your initialize won't work without any arguments; it needs at least one. The result is a pretty cryptic error if you didn't intend to write a should with an implicit subject.

RSpec test ArgumentError on method with parameters

I'm having problems getting this simple test to pass on RSpec 2.8.
I want to write a simple test for the absence of parameters on a method that requires them (i.e. ArgumentError: wrong number of arguments ('x' for 'y')).
My test is testing a Gem module method like so:
describe "#ip_lookup" do
it "should raise an ArgumentError error if no parameters passed" do
expect(#geolocater.ip_lookup).to raise_error(ArgumentError)
end
end
My gem module code looks like this:
module Geolocater
def ip_lookup(ip_address)
return ip_address
end
end
My spec runs with this output.
Failure/Error: expect(#geolocater.ip_lookup).to raise_error(ArgumentError)
ArgumentError:
wrong number of arguments (0 for 1)
# ./lib/geolocater.rb:4:in `ip_lookup'
# ./spec/geolocater_spec.rb:28:in `block (3 levels) in <top (required)>'
What am I missing here?
You need to pass a block to #expect, not a regular argument:
describe "#ip_lookup" do
it "should raise an ArgumentError error if no parameters passed" do
expect { #geolocater.ip_lookup }.to raise_error(ArgumentError)
end
end

Ruby. Mocking in RSpec

I have a problem with mocking. I have class DistanceMatrix and I would
like to indicate which method form_matrix was called in if/else
statement. I need to use mocha and RSpec. Any ideas?
class DistanceMatrix
def initialize(*args)
if args[0].class == String
form_matrix(get_data_from_yaml(args[0], args[1]))
elsif args[0].class == Array || args[0] == nil
form_matrix(get_data_from_db(args[0]))
end
end
def form_matrix(...)
...
end
end
it tried:
describe DistanceMatrix, "when mocking ..." do
it "should do call form_matrix" do
DistanceMatrix.any_instance.expects(:form_matrix).with([1]).once
DistanceMatrix.any_instance.expects(:get_data_from_yaml).with("file_name.yml").once.returns([1])
DistanceMatrix.new("file_name.yml")
end
end
but got error:
Failures:
1) DistanceMatrix when mocking ... should do call form_matrix
Failure/Error: DistanceMatrix.new("file_name.yml")
unexpected invocation: #<AnyInstance:DistanceMatrix>.get_data_from_yaml('file_name.yml', nil)
unsatisfied expectations:
- expected exactly once, not yet invoked: #<AnyInstance:DistanceMatrix>.get_data_from_yaml('file_name.yml')
- expected exactly once, not yet invoked: #<AnyInstance:DistanceMatrix>.form_matrix([1])
satisfied expectations:
- allowed any number of times, already invoked once: #<DistanceMatrix:0x9e48b40>.get_optimal_route(any_parameters)
- allowed any number of times, already invoked once: #<Database::Distances:0x9d59798>.load_distances(any_parameters)
# ./distance_matrix.rb:18:in `initialize'
# ./tsp_algorithm_spec.rb:253:in `new'
# ./tsp_algorithm_spec.rb:253:in `block (2 levels) in <top (required)>'
Finished in 0.25979 seconds
I found that in RSpec we should use not .expects() but .should_receive(), so I tried:
describe DistanceMatrix, "when mocking ..." do
it "should do call form_matrix" do
DistanceMatrix.any_instance.should_receive(:form_matrix).with([1])
DistanceMatrix.any_instance.should_receive(:get_data_from_yaml).with("file_name.yml").and_return([1])
DistanceMatrix.new("file_name.yml")
end
end
but got new failure:
Failures:
1) DistanceMatrix when mocking ... should do call form_matrix
Failure/Error: DistanceMatrix.any_instance.should_receive(:form_matrix).with([1])
(#<Mocha::ClassMethods::AnyInstance:0x96356b0>).form_matrix([1])
expected: 1 time
received: 0 times
# ./tsp_algorithm_spec.rb:251:in `block (2 levels) in <top (required)>'
Finished in 0.26741 seconds
I only have experience with using Mocha and not RSpec, but looking at the Mocha failure message, the key parts are these :-
unexpected invocation: #<AnyInstance:DistanceMatrix>.get_data_from_yaml('file_name.yml', nil)
unsatisfied expectations:
- expected exactly once, not yet invoked: #<AnyInstance:DistanceMatrix>.get_data_from_yaml('file_name.yml')
If you look at the ends of these lines, you will notice that get_data_from_yaml is not being called with the expected parameters. It is being called with ('filename.yml', nil) and not ('filename.yml') as expected.
This is happening because when you call DistanceMatrix.new("file_name.yml") in your test with only one argument and then inside DistanceMatrix#initialize DistanceMatrix#get_data_from_yaml is being called with (args[0], args[1]) and since args is a single element array, args[1] will be nil.
Maybe this isn't how you expected Ruby to work, but the following demonstrates this behaviour :-
def foo(*args)
puts "args[0]=#{args[0].inspect}; args[1]=#{args[1].inspect}"
end
foo("string") # => args[0]="string"; args[1]=nil
DistanceMatrix.any_instance.expects(:form_matrix).with("String") # => supply the correct string param
or
DistanceMatrix.any_instance.expects(:form_matrix).with([]) # => supply the correct array param
I'm not sure what your get_data_from_db and get_data_from_yaml methods are doing, but you should be able to control those inputs as well to verify the correct arguments are being supplied to form_matrix.
EDITED
You'll have to use DistanceMatrix.any_instance instead of mocking on an instance variable because you're trying to mock something in the initializer. Also, in case its unclear, you'll need to actually make the appropriate method call after you set up the mock in the lines above, e.g.
DistanceMatrix.new("SomeString")
EDITED
it "should do call #form_matrix with proper arguments" do
DistanceMatrix.any_instance.expects(:form_matrix).with([1])
DistanceMatrix.any_instance.expects(:get_data_from_yaml).with("foo").returns([1])
DistanceMatrix.new("foo")
end

Resources