is it good to use global variables in specs?
Global variables are discouraged, in RSpec, and in general. Here's why:
Modifying a global variable anywhere affects code everywhere.
So, if a test ever modified the $now
variable, all subsequent tests would be affected by the modification. This could lead to some very hard to debug test failures. Hard to debug, because the test would be correct. But, the value of the global would not. So, you could not find the bug in the tests by simple inspection. You'd have to debug it at runtime.
Automated tests need to be independent of each other. This is why RSpec has the lazily executed let
statement. let
allows you to define a variable within the context of a single example. Here's what the RSpec docs say:
Use let to define a memoized helper method. The value will be cached across
multiple calls in the same example but not across examples.
What this means is that if you define now
like this:
let(:now) { DateTime.parse('2020-01-01 00:00:01 -0500') }
The value is guaranteed to be exactly what you say it is in every single test. Because the value is memoized on a per example basis, one test can never affect the value that another test receives when executing.
If you're still not convinced, I would recommend using a global constant over a global variable. At least then, Ruby would warn you if you tried to change the value.
Set variable once for all examples in RSpec suite (without using a global variable)
for this purpose I usually write custom modules that I can include in the spec_helper.rb
file.
Let's say that I am testing a back-end API and I don't want to parse every time the JSON response body.
spec/
spec/spec_helper.rb
spec/support/request_helper.rb
spec/controllers/api/v1/users_controller_spec.rb
I first define a function in a module placed under the support subfolder.
# request_helper.rb
module Request
module JsonHelpers
def json_response
@json_response ||= JSON.parse(response.body, symbolize_names: true)
end
end
end
Then I include this module by default for some test types
#spec_helper.rb
#...
RSpec.configure do |config|
config.include Request::JsonHelpers, type: :controller
end
Then I use methods defined in the test.
# users_controller_spec.rb
require 'rails_helper'
RSpec.describe Api::V1::UsersController, type: :controller do
# ...
describe "POST #create" do
context "when is successfully created" do
before(:each) do
@user_attributes = FactoryGirl.attributes_for :user
post :create, params: { user: @user_attributes }
end
it "renders the json representation for the user record just created" do
user_response = json_response[:user]
expect(user_response[:email]).to eq(@user_attributes[:email])
end
it { should respond_with 201 }
end
end
In your case, you could create a module such as
module EnvHelper
def is_debug_mode?
@debug_mode ||= ENV['DEBUG']
end
end
Then you can include it and simply use the method is_debug_mode?
in your tests.
Rails: RSpec: using global path variables only seems to work within it blocks
Yes, named routes work only within it
or specify
blocks. But it's easy to modify the code:
def should_be_stable(path)
get path
response.status.should be(200)
end
describe "StaticPages" do
describe "Home => GET" do
it { should_be_stable(root_path) }
end
end
You steel need to include url_helpers
Rspec: How to assign instance variable in controller spec
When testing private methods in controllers, rather than use send
, I tend to use an anonymous controller due to not wanting to call the private method directly, but the interface to the private method (or, in the test below, effectively stubbing that interface). So, in your case, perhaps something like:
require 'spec_helper'
describe TestController do
controller do
def test_some_method
some_method
end
end
describe "a phone test with some_method" do
subject { controller.test_some_method }
context "when my_variable is not nil" do
before { controller.instance_variable_set(:@my_variable, Phone.new(...)) }
it { should be_true }
end
context "when my_variable is nil" do
before { controller.instance_variable_set(:@my_variable, nil) }
it { should_not be_true } # or should be_false or whatever
end
end
end
There's some good discussion on the issue of directly testing private methods in this StackOverflow Q&A, which swayed me towards using anonymous controllers, but your opinion may differ.
RSpec - Using `let` in spec helper
You might consider other approaches:
- Use mock objects instead of real ones.
- Refactor the initializer and extract the slow operation to another method
Mock objects of course bring their own set of drawbacks; they can become stale and make tests more brittle. But for some tests that is not an issue.
Refactoring initializers is a favorite of mine. E.g.
MyBigObject.new(args)
becomes
MyBigObject.new(args).setup
or :load_data
or :connect_to_database_on_the_moon
or whatever is taking a long time. You get the picture.
Obviously this means changing your code, but I find that often works out to be helpful in other ways, and it certainly makes testing easier.
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.
Related Topics
Is the Current Ruby Method Called via Super
Controller Monkey Patch in Initializer Gets Lost When Rails Reloads Classes
How to Run an Excel MACro from Ruby
Cancan Abilities in Separate File
Validate That String Contains Only Allowed Characters in Ruby
How to Get Today's Date in Ruby 1.9.3
What Is the Advantage of Creating an Enumerable Object Using To_Enum in Ruby
How Is Ruby Tcpsocket Timeout Defined
Svg Files in Raphael, Can They Be Used
Why Does the Break Statement in Ruby Behave Differently When Using Proc.New V. the Ampersand Sign
Should I Be Using Rails or Ruby for This Website Application? How
Encrypting/Decrypting 3Des in Ruby
Bundle Install' Fails (Because of Git Protocol)
Using Ruby, What Is the Most Efficient Way to Get the Content Type of a Given Url
Sinatra Helper to Fake a Request
How to Make a Ruby Script Using Trollop for Command Line Parsing