RSpec 3.5 pass argument to shared_context - ruby-on-rails

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.

Related

Getting FactoryBot object attributes for API requests with RSpec

I am setting up RSpec request tests, and I have the following test:
require 'rails_helper'
RSpec.describe "ClientApi::V1::ClientContexts", type: :request do
describe "POST /client_api/v1/client_contexts" do
let(:client_context) { build :client_context }
it "creates a new context" do
post "/client_api/v1/client_contexts", params: {
browser_type: client_context.browser_type,
browser_version: client_context.browser_version,
operating_system: client_context.operating_system,
operating_system_version: client_context.operating_system_version
}
expect(response).to have_http_status(200)
expect(json.keys).to contain_exactly("browser_type", "browser_version", "operating_system", "operating_system_version")
# and so on ...
end
end
end
The corresponding factory is this:
FactoryBot.define do
factory :client_context do
browser_type { "Browser type" }
browser_version { "10.12.14-blah" }
operating_system { "Operating system" }
operating_system_version { "14.16.18-random" }
end
end
Now, obviously, that all seems a bit redundant. I have now three places in which I specify the attributes to be sent. If I ever want to add an attribute, I have to do it in all of these places. What I actually want to do is send the particular attributes that the Factory specifies via POST, and then check that they get returned as well.
Is there any way for me to access the attributes (and only these!) that I defined in the Factory, and re-use them throughout the spec?
I should prefix this with a warning that abstracting away the actual parameters from the request being made could be seen as detrimental to the overall test expressiveness. After all, now you'd have to look into the Factory to see which parameters are sent to the server.
You can simply get the Factory-defined attributes with attributes_for:
attributes_for :client_context
If you need more flexibility, you can implement a custom strategy that returns an attribute Hash from your Factory without creating the object, just building it.
Create a file spec/support/attribute_hash_strategy.rb:
class AttributeHashStrategy
def initialize
#strategy = FactoryBot.strategy_by_name(:build).new
end
delegate :association, to: :#strategy
def result(evaluation)
evaluation.hash
end
end
Here, the important part is evaluation.hash, which returns the created object as a Ruby Hash.
Now, in your rails_helper.rb, at the top:
require 'support/attribute_hash_strategy'
And below, in the config block, specify:
# this should already be there:
config.include FactoryBot::Syntax::Methods
# add this:
FactoryBot.register_strategy(:attribute_hash, AttributeHashStrategy)
Now, in the Spec, you can build the Hash like so:
require 'rails_helper'
RSpec.describe "ClientApi::V1::ClientContexts", type: :request do
describe "POST /client_api/v1/client_contexts" do
let(:client_context) { attribute_hash :client_context }
it "creates a new context" do
client = create :client
post "/client_api/v1/client_contexts",
params: client_context
expect(response).to have_http_status(200)
end
end
end
The attribute_hash method will be a simple Hash that you can pass as request parameters.

Rails/RSpec: Memoized/cached block on a context level e.g. before(:context) referencing a let

While writing specs, I often have a expensive operation I'd like to test multiple aspects of without needing to call it each time. How do I do this, while still referencing a greater scoped variable?
describe Hq::UsersController
let(:create_params) {
{'name': 'Bob',
'email': 'bob#burgers.com'}
}
describe "#create" do
context "when a user does not already exist" do
before(:context) do
# (some expensive operation here)
VCR.use_cassette('external_user_check') do
# note: this will error out, see message below
post :create, :params => create_params
end
end
it "creates the user and dependent objects" do
expect(User.count).to be(1)
user = User.first
expect(user.name).to eq(create_params['name'])
end
it "returns an 204 (no content) status" do
expect(response).to have_http_status(:no_content)
end
end
context "when a user already exists" do
before(:context) do
User.create(email: create_params['email'])
end
it "raises a RecordInvalid error and does not create any objects" do
VCR.use_cassette('external_user_check') do
expect { post :create, :params => create_params }.to raise_error(ActiveRecord::RecordInvalid)
end
# I wish I could break this into two `it` blocks :(
expect(User.count).to be(0)
end
end
end
end
This is the error I get:
`let` and `subject` declarations are not intended to be called
in a `before(:context)` hook, as they exist to define state that
is reset between each example, while `before(:context)` exists to
define state that is shared across examples in an example group.
I understand the intent of warning, but in my case, how can we run or cache an expensive operation just one time, while also reusing some shared state across contexts? Generally when I write specs, I assume the state is the same within a context.
Right now, it seems my only solution is to copy and paste the params in multiple context blocks for this to be correct. However, that would costly to maintain if the params change or if the params are lengthy.
I'm open to refactoring suggestions or if this happens to indicative of some spec code smell. Thanks!

How to avoid Rspec shared examples 'previously defined' warning?

I am trying to learn how to use Rspec's shared examples feature and am getting a warning when I run my tests:
WARNING: Shared example group 'required attributes' has been previously defined at:
/Users/me/app/spec/support/shared_examples/required_attributes_spec.rb:1
...and you are now defining it at:
/Users/me/app/spec/support/shared_examples/required_attributes_spec.rb:1
The new definition will overwrite the original one.
....
I have read what I think is the documentation on this problem here but I'm having trouble understanding it/seeing the takeaways for my case.
Here is my shared example:
# spec/support/shared_examples/required_attributes_spec.rb
shared_examples_for 'required attributes' do |arr|
arr.each do |meth|
it "is invalid without #{meth}" do
subject.send("#{meth}=", nil)
subject.valid?
expect(subject.errors[meth]).to eq(["can't be blank"])
end
end
end
I am trying to use this in a User model and a Company model. Here is what it looks like:
# spec/models/user_spec.rb
require 'rails_helper'
describe User do
subject { build(:user) }
include_examples 'required attributes', [:name]
end
# spec/models/company_spec.rb
require 'rails_helper'
describe Company do
subject { build(:company) }
include_examples 'required attributes', [:logo]
end
Per the recommendations in the Rspec docs I linked to above, I have tried changing include_examples to it_behaves_like, but that didn't help. I also commented out company_spec.rb entirely so there was just one spec using the shared example, and I am still getting the warning.
Can anyone help me see what's really going on here and what I should do in this case to avoid the warning?
I found the answer in this issue at the Rspec Github:
Just in case someone googles and lands here. If putting your file with
shared examples into support folder has not fixed the following
error...Make sure your filename does not end with _spec.rb.
As a followup to this, I had the issue in an included shared context with a filename that did not end in _spec.rb and was manually loaded via require_relative, not autoloaded. In my case, the issue was a copy-paste problem. The test looked like this:
RSpec.shared_examples 'foo' do
RSpec.shared_examples 'bar' do
it ... do... end
it ... do... end
# etc.
end
context 'first "foo" scenario' do
let ...
include_examples 'bar'
end
context 'second "foo" scenario' do
let ...
include_examples 'bar'
end
end
The intent was to provide a single shared set of examples that exercised multiple contexts of operation for good coverage, all running through that internal shared examples list.
The bug was simple but subtle. Since I have RSpec monkey patching turned off (disable_monkey_patching!), I have to use RSpec.<foo> at the top level. But inside any other RSpec blocks, using RSpec.<foo> defines the entity inside RSpec's top-level :main context. So, that second set of shared, "internal" examples weren't being defined inside 'foo', they were being defined at the top level. This confused things enough to trigger the RSpec warning as soon more than one other file require_relative'd the above code.
The fix was to just do shared_examples 'bar' in the nested set, not RSpec.shared_examples 'bar', so that the inner examples were correctly described inside the inner context.
(Aside: This is an interesting example of how having monkey patching turned off is more important than might at first glance appear to be the case - it's not just for namespace purity. It allows for a much cleaner distinction in declarations between "this is top level" and "this is nested".)

Create RSpec context inside a function

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.

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