How to call subject several times with RSpec - ruby

I create RSpec test below. I try to call subject several times. But I cannot get expected result. I call subject three times, doesn't it? So, I expect three Book records. Does subject cannot call one time?
require 'rails_helper'
RSpec.describe Book, type: :model do
context 'some context' do
subject { Book.create }
before do
subject
end
it 'something happen' do
subject
expect { subject }.to change{ Book.count }.from(0).to(3)
end
end
end

No. let and subject are memoized (and lazy-loaded).
You can change it like this
subject { 3.times { Book.create } }
it 'something happen' do
expect { subject }.to change{ Book.count }.from(0).to(3)
end
Or if you (for whatever reason) want to call something 3 times - define a method:
subject { create_book }
def create_book
Book.create
end
before do
create_book
end
it 'something happen' do
create_book
expect { subject }.to change{ Book.count }.from(2).to(3)
end
Then it will be called 3 times: once in before block, one in it before expectation and once inside the expectation (but the change will be from 2 not from 0, because those 2 times were called before)

If you don't want to memoize a result you can also use a Proc
describe MyClass do
let(:param_1) { 'some memoized string' }
let(:described_method) { Proc.new { described_class.do_something(param_1) } }
it 'can run the method twice without memoization' do
expect(described_method.call).to have_key(:success) # => {success: true}
expect(described_method.call).to have_key(:error) # => {error: 'cant call do_something with same param twice'}
end
end

Related

RSpec how to stub out yield and have it not hit ensure

I have an around action_action called set_current_user
def set_current_user
CurrentUser.set(current_user) do
yield
end
end
In the CurrentUser singleton
def set(user)
self.user = user
yield
ensure
self.user = nil
end
I cannot figure out how to stub out the yield and the not have the ensure part of the method called
Ideally I would like to do something like
it 'sets the user' do
subject.set(user)
expect(subject.user).to eql user
end
Two errors I am getting
No block is given
When I do pass a block self.user = nil gets called
Thanks in advance
A few things to point out that might help:
ensure is reserved for block of codes that you want to run no matter what happens, hence the reason why your self.user will always be nil. I think what you want is to assign user to nil if there's an exception. In this case, you should be using rescue instead.
def set(user)
self.user = user
yield
rescue => e
self.user = nil
end
As for the unit test, what you want is to be testing only the .set method in the CurrentUser class. Assuming you have everything hooked up correctly in your around filter, here's a sample that might work for you:
describe CurrentUser do
describe '.set' do
let(:current_user) { create(:user) }
subject do
CurrentUser.set(current_user) {}
end
it 'sets the user' do
subject
expect(CurrentUser.user).to eq(current_user)
end
end
end
Hope this helps!
I am not sure what you intend to accomplish with this as it appears you just want to make sure that user is set in the block and unset afterwards. If this is the case then the following should work fine
class CurrentUser
attr_accessor :user
def set(user)
self.user = user
yield
ensure
self.user = nil
end
end
describe '.set' do
subject { CurrentUser.new }
let(:user) { OpenStruct.new(id: 1) }
it 'sets user for the block only' do
subject.set(user) do
expect(subject.user).to eq(user)
end
expect(subject.user).to be_nil
end
end
This will check that inside the block (where yield is called) that subject.user is equal to user and that afterwards subject.user is nil.
Output:
.set
sets user for the block only
Finished in 0.03504 seconds (files took 0.14009 seconds to load)
1 example, 0 failures
I failed to mention I need to clear out the user after every request.
This is what I came up with. Its kinda crazy to put the expectation inside of the lambda but does ensure the user is set prior to the request being processed and clears it after
describe '.set' do
subject { described_class }
let(:user) { OpenStruct.new(id: 1) }
let(:user_expectation) { lambda{ expect(subject.user).to eql user } }
it 'sets the user prior to the block being processed' do
subject.set(user) { user_expectation.call }
end
context 'after the block has been processed' do
# This makes sure the user is always cleared after a request
# even if there is an error and sidekiq will never have access to it.
before do
subject.set(user) { lambda{} }
end
it 'clears out the user' do
expect(subject.user).to eql nil
end
end
end

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 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