RSpec, implicit subject, and exceptions - ruby

Is there a way to properly test exception raising with implicit subjects in rspec?
For example, this fails:
describe 'test' do
subject {raise 'an exception'}
it {should raise_exception}
end
But this passes:
describe 'test' do
it "should raise an exception" do
lambda{raise 'an exception'}.should raise_exception
end
end
Why is this?

subject accepts a block which returns the subject of the remainder.
What you want is this:
describe 'test' do
subject { lambda { raise 'an exception' } }
it { should raise_exception }
end
Edit: clarification from comment
This:
describe 'test' do
subject { foo }
it { should blah_blah_blah }
end
is more or less equivalent to
(foo).should blah_blah_blah
Now, consider: without the lambda, this becomes:
(raise 'an exception').should raise_exception
See here that the exception is raised when the subject is evaluated (before should is called at all). Whereas with the lambda, it becomes:
lambda { raise 'an exception' }.should raise_exception
Here, the subject is the lambda, which is evaluated only when the should call is evaluated (in a context where the exception will be caught).
While the "subject" is evaluated anew each time, it still has to evaluate to the thing you want to call should on.

The other answer explains the solution pretty well. I just wanted to mention that RSpec has a special helper called expect. It's just a little easier to read:
# instead of saying:
lambda { raise 'exception' }.should raise_exception
# you can say:
expect { raise 'exception' }.to raise_error
# a few more examples:
expect { ... }.to raise_error
expect { ... }.to raise_error(ErrorClass)
expect { ... }.to raise_error("message")
expect { ... }.to raise_error(ErrorClass, "message")
More information can be found in the RSpec documentation on the built-in matchers.

Related

How to use check something before an error is raised

I have the following ruby code
class Gateway
...
def post
begin
...
raise ClientError if state == :open
rescue ClientError => e
Log.add("error")
raise
end
end
end
On RSpec, how can I check that when ClientError is raised Log.add is called?
I have tried different things but I always get the error raised.
Thanks
You can probably do something like this (the initialize step might need to look bit different, depending on how you need to set the state to :open):
describe 'Gateway#post' do
let(:gateway) { Gateway.new(state: :open) }
before { allow(Log).to receive(:add) }
it 'raises an excpetion' do
expect { gateway.post }.to raise_error(ClientError)
expect(Log).to have_received(:add).with('error')
end
end
Something like this should work:
describe '#post' do
context 'with state :open' do
let(:gateway) { Gateway.new(state: :open) }
it 'logs the error' do
expect(Log).to receive(:add).with('error')
gateway.post rescue nil
end
it 're-raises the error' do
expect { gateway.post }.to raise_error(ClientError)
end
end
end
In the first example, rescue nil ensures that your spec is not failing because of the raised error (it silently rescues it). The second example checks that the error is being re-raised.

How can I check if my subject raises an exception?

I'm currently creating an object in subject and need to test if this raises an exception. The following code illustrates what I'm trying to achieve:
describe MyClass do
describe '#initialize' do
subject { MyClass.new }
it { is_expected.not_to raise_error(Some::Error) }
end
end
I have a feeling I'm going about this the wrong way. What is the preferred way to set the subject to a new object, without creating the object twice?
Update
My problem was two-fold. Firstly, this syntax does not work:
it { is_expected.not_to raise_error }
Using expect inside an it block does, however (as pointed out by Jimmy Cuadra):
it 'does not raise an error' do
expect { subject }.not_to raise_error
end
I am not well enough acquainted with RSpec to tell you why this is.
Secondly, since RSpec 3.0.0.beta1, it is longer possible to use raise_error with a specific error class. The following, therefore, is invalid:
expect { subject }.to raise_error(Some::Error)
For more information, see
Rspec 3.0.0.beta1 changelog
Consider deprecating `expect { }.not_to raise_error(SpecificErrorClass)` #231
Remove expect {}.not_to raise_error(SomeSpecificClass) #294
If I'm understanding correctly, you're trying to test if instantiating a class causes an exception. You would just do this:
describe MyClass do
it "doesn't raise an exception when instantiated" do
expect { subject }.not_to raise_error
end
end
The right way to do that through is_expected syntax is to wrap your subject value by a Proc, like the following example:
describe MyClass do
describe '#initialize' do
subject { -> { MyClass.new } }
it { is_expected.not_to raise_error(Some::Error) }
end
end
This way is more accurate, because sometimes your use case is to expect that specific kinds of exceptions should not be thrown (while others are allowed to be thrown). This approach will cover such use cases.

Simplifying rspec unit tests

A lot of times in unit test in rspec having to specify both a context and a let is somewhat cumbersome and seems unnecessary. For example:
context 'type = :invalid' do
let(:type) { :invalid }
it { expect { subject }.to raise_error(ArgumentError) }
end
It would be nicer (in aggregate over lots of tests) if I could do something like:
let_context type: :invalid do
it { expect { subject }.to raise_error(ArgumentError) }
end
The method would define a context and let(s) for me and the context's argument would be something like type = :invalid or let(:type) { :invalid } because I don't have anything else to say other that the fact that this variable has changed.
A lot of times in unit test in rspec having to specify both a context and a let is somewhat cumbersome
Sounds like you might want to use a RSpec shared context.
UPDATE
RSpec provides a DSL for the syntax you're suggesting: a shared example. For example:
RSpec.shared_examples "some thang" do |type|
it { expect { subject }.to raise_error(ArgumentError) }
end
RSpec.shared_examples "a thang" do
include_examples "some thang", :invalid
# Or whatever is more appropriate for your domain
# I.e., If you're testing subclass behavior use it_should_behave_like()
end
actually you could get less lines by following some http://betterspecs.org recommendations:
context 'type = :invalid' do
let(:type) { :invalid }
it { expect{ subject }.to raise_error(ArgumentError)
end
Your variant could be read as
it raises an error expect subject to raise error
While this is much cleaner
it expect subject to raise_error
Nevertheless it is pretty off topic :)
UPD
Oh. Really you can't pass two blocks to method, so below example is not valid Ruby :)
context(:type) { :invalid } do
it{ expect{ subject }.to raise_error(ArgumentError)
end
While your example
let_context type: :invalid do
...
end
Won't do lazy execution, like let does

How to write expect {}.to raise_error when rspec's syntax is configured with only should

I have this configuration on rspec:
config.expect_with :rspec do |c|
c.syntax = :should
end
It makes the expect {}.to raise_error invalid, how could I write this error raising test with should syntax?
I would suggest to use this only if the most-recent RSpec expect { code() }.to raise_error syntax is not available to you:
lambda { foo( :bad_param ) }.should raise_error
or
lambda { foo( :bad_param ) }.should raise_error( ArgumentError )
Replacing foo( :bad_param ) with whatever Ruby code you wish to assert fails, and ArgumentError with whatever exception class you expect the failure to raise.
In tests where I could use the expect syntax, I prefer to define that test in its own describe block, put the test content (ie expect { <this_content> }) into a stabby lambda, stick it in a new subject, and refer to it in an it block, like so:
describe "some test that raises error" do
let(:bad_statement) { something_that_raises_an_error }
subject { -> { bad_statement } }
it { should raise_error }
end
If you wanted, you could also just do away with the let statement altogether and put its content directly in the subject.

rspec expect throws "undefined method"

I am a complete Ruby newby and am playing around with rspec
I am testing a class (Account) that has this line:
attr_reader :balance
When I try to test it with this method:
it "should deposit twice" do
#acc.deposit(75)
expect {
#acc.deposit(50)
}.to change(Account.balance).to(125)
end
I get this error:
NoMethodError in 'Account should deposit twice'
undefined method `balance' for Account:Class
I don't understand why I get the error since the attribute 'balance' exists, however I can see that it is not a method, but shouldn't rspec be able to find it anyway?
Update:
As Jason noted I should be #acc.balance, since this is what I am asserting. But I get 'nil is not a symbol' when doing this.
It should be #acc.balance
it "should deposit twice" do
#acc = Account.new
#acc.deposit(75)
#acc.balance.should == 75
expect {
#acc.deposit(50)
}.to change(#acc, :balance).to(125)
end
i think it should be
expect {#acc.deposit(50)}.to change(#acc.balance}.to(125)
It should be:
it "should deposit twice" do
#acc.deposit(75)
expect {
#acc.deposit(50)
}.to change { #acc.balance }.to(125)
end
Please note that you need use curly braces { ... } instead of parentheses ( ... ) around #acc.balance. Otherwise #acc.balance is evaluated before it is passed to change method which expects either symbol or block.

Resources