Rspec raise matcher not working, copied syntax from docs? - ruby-on-rails

rspec-core (3.9.1)
rspec-expectations (3.9.0)
rspec-mocks (3.9.1)
rspec-rails (4.0.0.beta4, 3.9.0)
rspec-support (3.9.2)
According to docs: https://relishapp.com/rspec/rspec-expectations/v/3-9/docs/built-in-matchers/raise-error-matcher, this should work:
expect { raise StandardError }.to raise_error
Yet in my code, when I run JUST that spec by itself, I get:
Failures:
1) time rules should work
Failure/Error: expect(raise StandardError).to raise_error
StandardError:
StandardError
# ./spec/models/time_rules_spec.rb:87:in `block (2 levels) in <top (required)>'

Look closely at the error:
Failure/Error: expect(raise StandardError).to raise_error
That indicates that your failing test looks like this:
it '...' do
expect(raise StandardError).to raise_error
end
when it should look like this:
it '...' do
expect { raise StandardError }.to raise_error
end
Your version is equivalent to:
result = raise StandardError
expect(result).to raise_error
so the raise is triggered before raise_error can trap and check for the exception. If you pass a block to expect as in the documentation:
expect { raise StandardError }.to raise_error
then the raise_error matcher will have everything set up to trap and check for the exception.
As an aside, you'll probably want to be more explicit with your raise_error matcher:
expect { raise StandardError }.to raise_error(StandardError)
to avoid an overly-broad matcher and a warning like this:
WARNING: Using the raise_error matcher without providing a specific error or message risks false positives, since raise_error will match when Ruby raises a NoMethodError, NameError or ArgumentError, potentially allowing the expectation to pass without even executing the method you are intending to call.

Related

RSpec expect something before expect raise_error

Imagine there's a method that rescues and does some logging.
def do_something
# do stuff
some_client.call(var1)
rescue StandardError => e
# log some stuff.
Rails.logger.error("#{self.class} - Var 1 is #{var1}.") if e.is_a?(MyError)
raise
end
Then in the RSpec, I'd like to
assert the error is raised.
it logs the error
before do
allow(Rails.logger).to receive(:error)
allow(some_client).to receive(:call).and_raise(MyError)
end
it "logs the error" do
subject
expect(Rails.logger).to have_received(:error).with(/some message with var1/)
end
it "raises MyError" do
expect { subject }.to raise_error(MyError)
end
expect { subject }.to raise_error(MyError) part is working as expected, but how should I assert the logging? With the example code above, RSpec will report the error on the raised error without asserting the logging.
Just put them both in the same it. Expect that it raises an error and logs it.
it "raises MyError and logs it" do
expect { subject }.to raise_error(MyError)
expect(Rails.logger).to have_received(:error).with(/some message with var1/)
end
Alternatively if you really want to check that it logs the error in a separate it you'll have to rescue the error. Otherwise your spec will fail (unhandled error)
it "logs the error" do
subject
rescue
ensure
expect(Rails.logger).to have_received(:error).with(/some message with var1/)
end

rspec: how to test the ensure block after raised an error

Here's my begin..rescue..ensure block. I want to write some test cases that after error is raised, the final result {} will be returned.
I am using rspec 3.3.
def external_call
result = ExternalApi.call
rescue => e
# handle the error, and re-raise
Handler.handle(e)
raise
ensure
result.presence || {}
end
I have wrote test case for the rescue part:
context 'when external api raise error' do
it 'handles the error, and re-raise' do
allow(ExternalApi).to receive(:call).and_raise(SomeError)
expect(Handler).to receive(:handle).with(e)
expect { subject.external_call }.to raise_error(SomeError)
end
end
But I am not sure how to test the ensure part after the error is re-raised.
Here's my attempt:
it 'returns {} after error raised' do
allow(ExternalApi).to receive(:call).and_raise(SomeError)
result = subject.external_call
expect(result).to eq({})
end
In this case, the test case will fail in the subject.external_call line, since it will raise error there. I am not sure how to test this cases after the error is re-raised.
When using begin/rescue/ensure block with implicit returns, ruby will return the last method to be run in the rescue block as the return value, not the ensure. If the value from the ensure block needs to be returned, it will either have to be explicitly returned, or not included in an ensure but instead moved outside of the begin/rescue block.
Below is an example which shows the difference.
class TestClass
def self.method1
raise 'an error'
rescue
'rescue block'
ensure
'ensure block'
end
def self.method2
raise 'an error'
rescue
'rescue block'
ensure
return 'ensure block'
end
def self.method3
begin
raise 'an error'
rescue
'rescue block'
end
'ensure equivalent block'
end
end
RSpec.describe TestClass do
it do
# does not work, method1 returns 'rescue block'
expect(TestClass.method1).to eql 'ensure block'
end
it do
# does work, as method2 explicitly returns 'ensure block'
expect(TestClass.method2).to eql 'ensure block'
end
it do
# does work, as method3 uses 'ensure equivalent block' as the inferred return
expect(TestClass.method3).to eql 'ensure equivalent block'
end
end

Writing rspec to test for error, but error is thrown and stops test from passing

I wrote a test where I expect the result to throw an error.
it{ Authentication.create_with_omniauth!(nil, nil).should raise_error }
The result indeed throws an error, but the validation fails. Apparently it's throwing an error. :)
2) Authentication methods: self.create_with_omniauth!: with bad input
Failure/Error: it{ Authentication.create_with_omniauth!(nil, nil).should raise_error }
ArgumentError:
ArgumentError
# ./app/models/authentication.rb:13:in `create_with_omniauth!'
# ./spec/models/authentication_spec.rb:69:in `block (5 levels) in <top (required)>'
Now what? How do you test for an error?
You should use expect {}.to raise_error syntax
expect { Authentication.create_with_omniauth!(nil, nil) }.to raise_error
or
lambda { Authentication.create_with_omniauth!(nil, nil) }.should raise_error
if you use Rspec 1.X.X
check out documentation
https://www.relishapp.com/rspec/rspec-expectations/docs/built-in-matchers/raise-error-matcher#expect-any-error
http://rspec.rubyforge.org/rspec/1.2.9/classes/Spec/Matchers.html#M000176
I'm not experienced with omniauth, but does something like this work?
describe "authentication" do
context "when nil" do
let(:nil_authentication) { Authentication.create_with_omniauth!(nil, nil) }
subject { -> { nil_authentication } }
it { should raise_error }
end
end

Rails Tutorial mass assignment security exception

I've been working through the Ruby on Rails Tutorial. I've run into a problem getting a test to pass that checks for the a mass assignment security exception to be thrown. I'm not sure why I'm getting this test failure, or how to fix it.
rspec:
describe "accessible attributes" do
it "should not allow access to user_id" do
expect do
Micropost.new(user_id: user.id)
end.should raise_error(ActiveModel::MassAssignmentSecurity::Error)
end
end
Failures:
1) Micropost accessible attributes should not allow access to user_id
Failure/Error: expect { Micropost.new(user_id: user.id) }.should raise_error(ActiveModel::MassAssignmentSecurity::Error)
expected ActiveModel::MassAssignmentSecurity::Error, got #<NoMethodError: undefined method `call' for #<RSpec::Expectations::ExpectationTarget:0x8af2bb8>>
# ./spec/models/micropost_spec.rb:23:in `block (3 levels) in <top (required)>
Try using to instead of should for your expect raise_error matcher.
describe "accessible attributes" do
it "should not allow access to user_id" do
expect do
Micropost.new(user_id: user.id)
end.to raise_error(ActiveModel::MassAssignmentSecurity::Error)
end
end

How to use RSpec's should_raise with any kind of exception?

I'd like to do something like this:
some_method.should_raise <any kind of exception, I don't care>
How should I do this?
some_method.should_raise exception
... doesn't work.
expect { some_method }.to raise_error
RSpec 1 Syntax:
lambda { some_method }.should raise_error
See the documentation (for RSpec 1 syntax) and RSpec 2 documentation for more.
RSpec 2
expect { some_method }.to raise_error
expect { some_method }.to raise_error(SomeError)
expect { some_method }.to raise_error("oops")
expect { some_method }.to raise_error(/oops/)
expect { some_method }.to raise_error(SomeError, "oops")
expect { some_method }.to raise_error(SomeError, /oops/)
expect { some_method }.to raise_error(...){|e| expect(e.data).to eq "oops" }
# Rspec also offers to_not:
expect { some_method }.to_not raise_error
...
Note: raise_error and raise_exception are interchangeable.
RSpec 1
lambda { some_method }.should raise_error
lambda { some_method }.should raise_error(SomeError)
lambda { some_method }.should raise_error(SomeError, "oops")
lambda { some_method }.should raise_error(SomeError, /oops/)
lambda { some_method }.should raise_error(...){|e| e.data.should == "oops" }
# Rspec also offers should_not:
lambda { some_method }.should_not raise_error
...
Note: raise_error is an alias for raise_exception.
Documentation: https://www.relishapp.com/rspec
RSpec 2:
https://www.relishapp.com/rspec/rspec-expectations/v/2-13/docs/built-in-matchers/raise-error-matcher
RSpec 1:
http://apidock.com/rspec/Spec/Matchers/raise_error
http://apidock.com/rspec/Spec/Matchers/raise_exception
Instead of lambda, use expect to:
expect { some_method }.to raise_error
This is applies for more recent versions of rspec, i.e. rspec 2.0 and up.
See the doco for more.
The syntax changed recently and now it is:
expect { ... }.to raise_error(ErrorClass)
From version 3.3 on rspec-expections gem raises a warning for a blank raise_error without a parameter
expect { raise StandardError }.to raise_error # results in warning
expect { raise StandardError }.to raise_error(StandardError) # fine
This gives you a hint that your code may fail with a different error than the test intended to check.
WARNING: Using the raise_error matcher without providing a specific error or message risks false positives, since raise_error will match when Ruby raises a NoMethodError, NameError or ArgumentError, potentially allowing the expectation to pass without even executing the method you are intending to call. Instead consider providing a specific error class or message. This message can be supressed by setting: RSpec::Expectations.configuration.warn_about_potential_false_positives = false.

Resources