Simplifying rspec unit tests - ruby

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

Related

How do you test custom Bugsnag meta_data in Ruby?

How do you test custom Bugsnag meta_data (in Ruby, with Rspec)?
The code that I want to test:
def do_something
thing_that_could_error
rescue => e
Bugsnag.notify(e) do |r|
r.meta_data = { my_extra_data: "useful info" }
end
end
The test I want to write:
context "when there's an error" do
it "calls Bugsnag with my special metadata" do
expect(Bugsnag).to receive(:notify) # TODO test meta_data values contain "my useful info"
expect do
do_something() # exception is thrown and rescued and sent to Bugsnag
end.not_to raise_error
end
end
I am using:
Ruby 2.6.6
Rspec 3.9.0
Bugsnag 6.17.0 https://rubygems.org/gems/bugsnag
The data inside of the meta_data variable is considerably more complicated than in this tiny example, which is why I want to test it. In a beautiful world, I would extract that logic to a helper and test the helper, but right now it is urgent and useful to test in situ.
I've been looking at the inside of the Bugsnag gem to figure this out (plus some Rspec-fu to capture various internal state and returned data) but at some point it's a good idea to ask the internet.
Since the metadata is complicated, I'd suggest simplifying it:
def do_something
thing_that_could_error
rescue => e
Bugsnag.notify(e) do |r|
r.meta_data = error_metadata(e, self, 'foo')
end
end
# I assumed that you'd like to pass exception and all the context
def error_metadata(e, object, *rest)
BugsnagMetadataComposer.new(e, object, *rest).metadata
end
So now you can have a separate test for BugsnagMetadataComposer where you have full control (without mocking) over how you initialize it, and test for metadata output.
Now you only have to test that BugsnagMetadataComposer is instantiated with the objects you want, metadata is called and it returns dummy hash:
let(:my_exception) { StandardError.new }
let(:mock_metadata) { Hash.new }
before do
# ensure thing_that_could_error throws `my_exception`
expect(BugsnagMetadataComposer)
.to receive(new)
.with(my_exception, subject, anything)
.and_return(mock_metadata)
end
And the hard part, ensure that metadata is assigned. To do that you can cheat a little and see how Bugsnag gem is doing it
Apparently there's something called breadcrumbs:
let(:breadcrumbs) { Bugsnag.configuration.breadcrumbs }
Which I guess has all the Bugsnag requests, last one on top, so you can do something similar to https://github.com/bugsnag/bugsnag-ruby/blob/f9c539670c448f7f129a3f8be7d412e2e824a357/spec/bugsnag_spec.rb#L36-L40
specify do
do_something()
expect(breadcrumbs.last.metadata).to eq(expected_metadata)
end
And for clarity, the whole spec would look a bit like this:
let(:my_exception) { StandardError.new }
let(:mock_metadata) { Hash.new }
before do
# ensure thing_that_could_error throws `my_exception`
expect(BugsnagMetadataComposer)
.to receive(new)
.with(my_exception, subject, anything)
.and_return(mock_metadata)
end
specify do
do_something()
expect(breadcrumbs.last.metadata).to eq(expected_metadata)
end

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.

How do I `expect` something which raises exception in RSpec?

I have a method which performs some actions on Cat model and in case of incorrect input raises an exception:
context "hungry cat" do
it { expect { eat(what: nil) }.to raise_error }
end
What I want to do is to check whether this method change cats status, like that:
context "hungry cat" do
it { expect { eat(what: nil) }.to raise_error }
it { expect { eat(what: nil) }.not_to change(cat, :status) }
end
The problem is that since eat(what: nil) will raise an exception the second it will fail no matter what. So, is it possible to ignore exception and check some condition?
I know that it's possible to do something like:
it do
expect do
begin
eat(what: nil)
rescue
end
end.not_to change(cat, :status)
end
But it's way too ugly.
You can chain positive assertions with and. If you want to mix in a negated one in the chain, RSpec 3.1 introduced define_negated_matcher.
You could do something like:
RSpec::Matchers.define_negated_matcher :not_change, :change
expect { eat(what: nil) }
.to raise_error
.and not_change(cat, :status)
Inspired by this comment.
You could use the "rescue nil" idiom to shorten what you already have:
it { expect { eat(what: nil) rescue nil }.not_to change(cat, :status) }
In RSpec 3 you can chain the two tests into one. I find this to be more readable than the rescue nil approach.
it { expect { eat(what: nil) }.to raise_error.and not_to change(cat, :status)}
It sounds strange that the eat(what: nil) code isn't run in isolation for each of your tests and is affecting the others. I'm not sure, but perhaps re-writing the tests slightly will either solve the issue or more accurately identify where the problem is (pick your flavour below).
Using should:
context "hungry cat" do
context "when not given anything to eat" do
subject { -> { eat(what: nil) } }
it { should raise_error }
it { should_not change(cat, :status) }
end
end
Using expect:
context "hungry cat" do
context "when not given anything to eat" do
let(:eating_nothing) { -> { eat(what: nil) } }
it "raises an error" do
expect(eating_nothing).to raise_error
end
it "doesn't change cat's status" do
expect(eating_nothing).to_not change(cat, :status)
end
end
end
You can also put expectations inside expectation blocks. It's still a little ugly, but it should work:
expect do
# including the error class is just good practice
expect { cat.eat(what: nil) }.to raise_error(ErrorClass)
end.not_to change { cat.status }

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, implicit subject, and exceptions

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.

Resources