Create RSpec context inside a function - ruby-on-rails

In order to avoid repeating myself a lot in my Rspec tests, I'd like to write a function like this
def with_each_partner(&block)
PARTNER_LIST.each do |partner|
context "with partner #{partner.name}" { yield partner }
end
end
I have such a function and it works in the sense that all the tests run with the correct value for partner supplied, but they don't print during output as being part of the "with partner X" context: instead if I have a test like this:
describe Thing do
subject { Thing.new(partner) }
with_each_partner do |partner|
it 'does its thing' do
expect(subject.do).to eq 'its thing'
end
end
end
I end up with output like this:
Thing
does its thing
Instead of the desired output, which is like:
Thing
with partner X
does its thing
with partner Y
does its thing
How can I get RSpec to correctly work with the context created in my function?

TL;DR: do this:
def with_each_partner(&block)
PARTNER_LIST.each do |partner|
context "with partner #{partner.name}" do
class_exec(&block)
end
end
end
Explanation
RSpec's DSL works by evaluating the blocks with a changed self -- this is how it is a method within a describe or context block, but not outside of it. When you yield, the provided block is evaluated with the original self that was self at the point the block was defined. What that means is that with your original with_each_partner definition, this code:
describe Thing do
subject { Thing.new(partner) }
with_each_partner do |partner|
it 'does its thing' do
expect(subject.do).to eq 'its thing'
end
end
end
Is really being evaluated like this:
describe Thing do
subject { Thing.new(partner) }
outer_example_group = self
with_each_partner do |partner|
outer_example_group.it 'does its thing' do
expect(subject.do).to eq 'its thing'
end
end
end
...and so the individual examples are defined on the outer example group, not on the "with partner #{partner.name}" nested group.
class_exec evaluates the provided block in the context of the class/module. In this case, the class is the example group subclass that RSpec has generated for your context. Using class_exec ensures that when it is called, the receiver is your nested context example group rather than the outer example group, creating the result you want.

Related

Use object created inside RSpec expect block in assertion

I'm trying to write a test where I need the value created by the expected block to write the assertion.
class Identification < ApplicationRecord
include Wisper::Publisher
after_save :publish_identification_declined
private
def publish_identification_declined
if status_previously_changed? && status == "declined"
broadcast(:identification_declined, identification: self)
end
end
end
I tried to do something like this but unfortunately identification_a ends up not being set.
require "rails_helper"
RSpec.describe Identification do
it "publish event identification_declined" do
identification_a = nil
expect { identification_a = create(:identification, :declined, id: 1) }
.to broadcast(:identification_declined, identification: identification_a)
end
end
I also have a feeling that this might not be a good idea.
An alternative could be using the instance_of matcher but then I don't know how to check if it's the right instance.
I think you shouldn't test the private functions because these are like black box and no matter how it works, as long as it returns as expected, obiously some times is necessary but in that case I think that the function shouldn't being private.
If you want test that the function is called, you can use receive rspec function. Some like:
require "rails_helper"
RSpec.describe Identification do
subject { described_class.new(identification: nil, :declined, id: 1) }
it "updates the state after save(or the event that should runs)" do
expect { subject }.to receive(:publish_identification_declined)
subject.save
end
end
You also can see the the documentation

How do you cause an expectation to halt execution in RSpec?

I want to test that a class receives a class-method call in RSpec:
describe MyObject do
it "should create a new user" do
expect(User).to receive(:new)
MyObject.new.doit
end
end
class MyObject
def doit
u = User.new
u.save
end
end
The problem is that the expectation does not halt execution. It simply stubs the class method .doit and continues execution.
The effect of the expectation is to ensure that User.new returns nil. So when we get to the next line which is User.save it then fails because there is no user object to call .save on.
I would like execution to halt as soon as the RSpec expectation has been satisfied - how can I do that?
nb
This is just an illustrative example - while an expect to change would work for User.new, it's not this actual code that I need to test
There is a great method for this and_call_original:
expect(User).to receive(:new).and_call_original
based on your test description, you're testing that a record was created, in those cases I would suggest you to do this:
expect {
MyObject.new.doit
}.to change{User.count}
or if you want to make sure it only created one:
expect {
MyObject.new.doit
}.to change{User.count}.by(1)

RSpec 3.5 pass argument to shared_context

I have this code that I want to reuse in several specs:
RSpec.shared_context "a UserWorker" do |user|
let(:mock_context_user) {{
id: 1,
brand: user.brand,
backend_token: user.backend_token
}}
before(:each) do
allow(SomeClass).to receive(:some_method)
.with(user.id).and_return(mock_context_user)
end
before(:each, context: true) do
Sidekiq::Testing.inline!
end
after(:each, context: true) do
Sidekiq::Testing.fake!
end
end
And in the spec file that uses the shared code:
let(:user) { build :user } # FactoryGirl
...
describe '#perform' do
# some lets here
include_context 'a UserWorker', user
context 'when something exists' do
it 'does some stuff' do
# test some stuff here
end
end
end
But that gives me this error:
/.rvm/gems/ruby-2.3.0#fb-cont/gems/rspec-core-3.5.1/lib/rspec/core/example_group.rb:724:in `method_missing': `user` is not available on an example group (e.g. a `describe` or `context` block). It is only available from within individual examples (e.g. `it` blocks) or from constructs that run in the scope of an example (e.g. `before`, `let`, etc). (RSpec::Core::ExampleGroup::WrongScopeError)
Suggestions? Any help is appreciated.
The RSpec docs aren't very clear on this, but you can inject additional values by passing a block containing let() calls to include_context. The "customization block" passed by the spec will be evaluated first, and is available to the code declared in the shared context.
Here's a shared context that depends on the specs including it to let() a value, value_from_spec, and then sets a couple more values, one via let() and one via a before() block:
RSpec.shared_context('a context', shared_context: :metadata) do
# assume the existence of value_from_spec
let(:a_value_from_context) { value_from_spec - 1 }
before(:each) do
# assume the existence of value_from_spec
#another_value_from_context = value_from_spec + 1
end
end
(Note that unlike the OP's |user| example, we never explicitly declare value_from_spec, we just trust that it'll be there when we need it. If you want to make what's going on more obvious, you could check defined?(:value_from_spec) and raise an error.)
And here's a spec that injects that value, and reads the shared context's transformations of it:
describe 'passing values to shared context with let()' do
# "customization block"
include_context 'a context' do
# set value_from_spec here
let(:value_from_spec) { 1 }
end
describe 'the context' do
it 'should read the passed value in a let() block' do
expect(a_value_from_context).to eq(0)
end
it 'should read the passed value in a before() block' do
expect(#another_value_from_context).to eq(2)
end
end
end
Since it will always return the same mock_context_user, you can try something more generic like:
allow(SomeClass)
.to receive(:some_method)
.with(an_instance_of(Fixnum))
.and_return(mock_context_user)
But I'm not actually sure if an_instance_of is available for RSpec 3.5, it is on RSpec 3.3.

Clean Rspec matcher for change(Model, :count).by(1)

I'm working hard trying to keep my spec files as clean as possible. Using 'shoulda' gem and writing customized matchers that follow the same pattern.
My question is about creating a custom matcher that would wrap expect{ post :create ... }.to change(Model, :count).by(1) and could be used in the same example groups with other 'shoulda' matchers. Details bellow:
Custom matcher (simplified)
RSpec::Matchers.define :create_a_new do |model|
match do |dummy|
::RSpec::Expectations::ExpectationTarget.new(subject).to change(model, :count).by(1)
end
end
Working example
describe 'POST create:' do
describe '(valid params)' do
subject { -> { post :create, model: agency_attributes } }
it { should create_a_new(Agency) }
end
end
This work OK as long as I use a subject lambda and my matcher is the only one in the example group.
Failing examples
Failing example 1
Adding more examples in the same group makes the other matcher fail because subject is now a lambda instead of an instance of the Controller.
describe 'POST create:' do
describe '(valid params)' do
subject { -> { post :create, model: agency_attributes } }
it { should create_a_new(Agency) }
it { should redirect_to(Agency.last) }
end
end
Failing example 2
The 'shoulda' matcher expect me to define a before block, but this become incompatible with my custom matcher
describe 'POST create:' do
describe '(valid params)' do
before { post :create, agency: agency_attributes }
it { should create_a_new(Agency) }
it { should redirect_to(Agency.last) }
end
end
Expected result
I am looking for a way to write my custom matcher that would fit in the same example group as other matchers, meaning my custom matcher should use the before block to execute the controller action, the "failing example #2" above is the way I would like to write my specs. Is it possible?
Thanks for reading
I do not think there is a way you can get your failing examples passing.
This is because change really needs a lambda, since it needs to perform your count twice (once before, and once after calling it). That's the reason I tend not to use it (or use it in context isolation).
What I usually do, instead of using the count matcher, is checking three things:
The record is persisted. If I assign the model to #model, then I use expect(assigns(:model)).to be_persisted
The record is an instance of the expected model (though might not seem useful, it is
quite descriptive when using an STI). expect(assigns(:model)).to be_a(Model).
Check the last record in DB is the same as the one I just create `expect(assigns(:model)).to eq(Model.last)``
And that's the way I usually test the change matcher without using it. Of course, you can now create your own matcher
RSpec::Matchers.define :create_a_new do |model|
match do |actual|
actual.persisted? &&
actual.instance_of?(Participant) &&
(Participant.last == actual)
end
end

RSpec Stub doesn't cover multiple nested Describe blocks

I have a test suite structured as follows:
describe ... do
[list of dates].each do
describe
before(:all) do
base_date = ...
end
describe ... do
[list of times].each do
describe ... do
before(:all) do
base_time = base_date + ...
DateTime.stub!(:now).and_return(base_time)
end
describe ... do
<test using records within date-time range based on base_time>
end
describe ... do
<another test using records within date-time range based on base_time>
end
end
end
end
end
end
end
The first test has DateTime(now) == base_time, but the second test as DateTime(now) == my computer's date-time, indicating that the stub is no longer in effect. Moving the stub! call into each describe loop resolves the problem, but I would like to understand why it doesn't work as written.
The reason lies probably elsewhere, stubs work fine with multiple nested describe blocks. Maybe :all vs :each is the problem: before(:all) is executed once before all describe blocks are executed, while before(:each) is executed each time before a describe block is executed.
Or maybe it has something to do with stubbing DateTime, have you tried
DateTime.any_instance.stub(:now).and_return(base_time)

Resources