How to declare a variable shared between examples in RSpec?
You should use before(:each)
or before(:all)
block:
describe Thing do
before(:each) do
@data = get_data_from_file # [ '42', '36' ]
end
it 'can read data' do
expect(@data.count).to eq 2
end
it 'can process data' do
expect(@data[0].to_i).to eq 42
end
end
The difference is that before(:each)
will be executed for each case separately and before(:all)
once before all examples in this describe/context
. I would recommend you to prefer before(:each)
over before(:all)
, because each example will be isolated in this case which is a good practice.
There are rare cases when you want to use before(:all)
, for example if your get_data_from_file
has a long execution time, in this case you can, of course, sacrifice tests isolation in favor of speed. But I want to aware you, that when using before(:all)
, modification of your @data
variable in one test(it
block) will lead to unexpected consequences for other tests in describe/context
scope because they will share it.
before(:all)
example:
describe MyClass do
before(:all) do
@a = []
end
it { @a << 1; p @a }
it { @a << 2; p @a }
it { @a << 3; p @a }
end
Will output:
[1]
[1, 2]
[1, 2, 3]
UPDATED
To answer you question
describe MyClass do
before(:all) do
@a = []
end
it { @a = [1]; p @a }
it { p @a }
end
Will output
[1]
[]
Because in first it
you are locally assigning instance variable @a, so it isn't same with @a in before(:all)
block and isn't visible to other it
blocks, you can check it, by outputting object_id
s. So only modification will do the trick, assignment will cause new object creation.
So if you are assigning variable multiple times you should probably end up with one it
block and multiple expectation in it. It is acceptable, according to best practices.
How to share a variable with many it examples in rspec
You can use lets within lets:
context "User" do
let(:email_address) { 'test@test.com' }
let(:user) { FactoryGirl.create(:user, email_address: email_address) }
You will then also have access to the email_address variable within all your tests.
This works because previously the email address was being randomly generated by the factory every time the user was created, as we hadn't set a value for it anywhere. So, we called the code below in each test:
send_preferences(user, "new")
It called the 'user' let which created a new user with a completely random email address (as we hadn't give it a specific email value). Therefore during the backend API call it was sending a different email address every time.
let(:user) { FactoryGirl.create(:user) }
However, when we defined the email address 'let' as 'test@test.com', and passed that into the user factory as in the code I provided, we overrode the randomly generated email address with our own static value, So, every time we call the code again:
send_preferences(user, "new")
It now triggers the user factory create which is also taking our new 'email_address' let, which is always set to a specific value of test@test.com every time it is called.
let(:email_address) { 'test@test.com' }
let(:user) { FactoryGirl.create(:user, email_address: email_address) }
Therefore, when the backend API call is made the email address is always what we set it to.
Also, as it is a let we can use that variable in any of the tests themselves if we wish. For example:
it 'should set the email address' do
expect(user.email_address).to eq(email_address)
end
It's quite hard to explain in a few sentences but let me know if that's still not clear.
How to pass an instance variable to an RSpec shared example
The problem is that self
within the example group is different from self
within a before
hook, so it's not the same instance variable even though it has the same name.
I recommend you use let
for cases like these:
# support/shared_examples.rb
shared_examples "a text field" do |field, fill, length|
it "it should be long enough" do
model.send("#{field}=", fill * length)
model.should be_valid
end
end
# company_spec.rb
describe Company do
describe "when address2" do
it_behaves_like "a text field", "address2", "a", Company.address2.limit do
let(:model) { Company.new( init stuff here ) }
end
end
end
RSpec Cannot Use Defined Variables in Shared Examples
The problem here is that the memoized let helper show_params
is called outside of an example.
Instead of passing the params you can simply reference a let
from the outer scope where you are including the example:
RSpec.describe UsersController, type: :controller do
let(:user) { FactoryGirl.create(:user) }
describe "GET #show" do
let(:action) { get :show, id: user }
it_should_behave_like "an authorized action"
end
end
RSpec.shared_examples "an authorized action" do
it "denies access" do
action
expect(response).to redirect_to(signin_url)
end
end
This is a pretty powerful pattern that lets you use a convention over configuration approach since the last let always wins.
RSpec.describe UsersController, type: :controller do
let(:user) { FactoryGirl.create(:user) }
describe "GET #show" do
let(:action) { get :show, id: user }
it_should_behave_like "an authorized action"
context "when signed in" do
before { sign_in user }
let(:action) { get :show, id: other_user }
context 'when viewing another user' do
it_should_behave_like "an authorized action"
end
end
end
end
RSpec: How to pass a let variable as a parameter to shared examples
From the docs, I think you need to put your let
statement in a block passed to it_behaves_like
:
let(:data) { "saved data" }
shared_examples "saves data to right place" do
it { expect(right_place.data).to eq data }
end
context "when something it saves to one place" do
it_behaves_like "saves data to right place" do
let(:right_place) { create(:place) }
end
end
context "when whatever it saves to other place" do
it_behaves_like "saves data to right place" do
let(:right_place) { create(:place) }
end
end
How to share RSpec let variables between shared contexts?
I'm not aware of a way to refer to the currently defined value of a let
variable in a let
block. (When I try it I get "stack level too deep".) I'd do what you're trying to do this way:
Rspec.shared_context 'JSON request' do
let(:common_headers) do
{
'Accept' => 'application/json'
}
end
let(:headers) { common_headers }
end
Rspec.shared_context 'Authenticated request' do
let(:headers) do
common_headers.merge('Authorization' => "Bearer #{token}")
end
end
How do I pass a variable two levels in a nested Rspec shared example?
You have recursion in your code:
let(:variable) { variable }
will call itself over and over again
Specs will pass down their variables automatically, so this will work:
shared_examples 'Foo' do
it_behaves_like 'Bar'
it_behaves_like 'Baz'
end
shared_examples 'Bar' do
it { expect(variable).to eq('value') }
end
and
describe SomeClass do
let(:variable) { 'value' }
it_behaves_like 'Foo'
end
Related Topics
Error Installing Any Ruby Version with Rvm on Osx
Best Way to Generate Order Numbers for an Online Store
Rails Routing (Root :To => ...)
Convert Datetime String to Utc in Rails
Vcrproxy: Record Phantomjs Ajax Calls with Vcr Inside Capybara
How to Run a Ruby Script Within Bundler Context
Iterate Every Month with Date Objects
How to Read Lines from File into Array
How to Use Active Record Without Rails
Rails -- Create and Create! Methods, Ror 3 Tutorial
Ruby Koans: Why Convert List of Symbols to Strings
Ruby Singleton Methods with (Class_Eval, Define_Method) VS (Instance_Eval, Define_Method)
Rails: How to Write Tests for a Ruby Module
Disable Rvm or Use Ruby Which Was Installed Without Rvm
Ruby Concatenate Strings and Add Spaces
How to Set a Blank Value for an F.Select Form Field