I have a gem, which has a method which acts differently depending on the Rails.env:
def self.env
if defined?(Rails)
Rails.env
elsif ...
And now I'd like to write a spec, which tests this code path. Currently I'm doing it like this:
Kernel.const_set(:Rails, nil)
Rails.should_receive(:env).and_return('production')
...
And it's ok, just feels ugly. Another way is to declare this in spec_helper
:
module Rails; end
And it works as well. But maybe there is a better way? Ideally this should work:
rails = double('Rails')
rails.should_receive(:env).and_return('production')
But, well, it does not. Or maybe I'm doing something wrong?
Per the various tweets about this, switching on constants is generally a bad idea because it makes things a bit of a challenge to test and you have to change the state of constants in order to do so (which makes them a little less than constant). That said, if you're writing a plugin that has to behave differently depending on the environment in which it's loaded, you're going to have to test on the existence of Rails
, Merb
, etc to somewhere, even if it's not in this particular part of the code. Wherever it is, you want to keep it isolated so that decision happens only once. Something like MyPlugin::env
. Now you can safely stub that method in most places, and then spec that method by stubbing constants.
As to how to stub the constants, your example doesn't look quite right. The code is asking if defined?(Rails)
, but Kernel.const_set(:Rails, nil)
doesn't undefine the constant, it just sets its value to nil
. What you want is something like this (disclaimer - this is off the top of my head, untested, not even run, may contain syntax errors, and is not well factored):
def without_const(const)
if Object.const_defined?(const)
begin
@const = const
Object.send(:remove_const, const)
yield
ensure
Object.const_set(const, @const)
end
else
yield
end
end
def with_stub_const(const, value)
if Object.const_defined?(const)
begin
@const = const
Object.const_set(const, value)
yield
ensure
Object.const_set(const, @const)
end
else
begin
Object.const_set(const, value)
yield
ensure
Object.send(:remove_const, const)
end
end
end
describe "..." do
it "does x if Rails is defined" do
rails = double('Rails', :env => {:stuff_i => 'need'})
with_stub_const(:Rails, rails) do
# ...
end
end
it "does y if Rails is not defined" do
without_const(:Rails) do
# ....
end
end
end
I'll give some thought as to whether we should include this in rspec or not. It's one of those things that if we added people would use it as an excuse to rely on constants when they don't need to :)
To test the condition where Rails is defined
mock_rails = mock(:env => mock)
Kernel.stub(:Rails).and_return(mock_rails)
To test the condition where Rails is undefined I believe you should not have to do anything otherwise you would define Rails and then the if defined?(Rails) condition would be true again
Modern way of stubbing global constants in RSpec involves using stub_const
. Lets say you have a method using Rails
, which you would like to test in an environment where Rails
is not defined (a rubygem for example):
def MyClass
def cache_value key, value
Rails.cache.write key, value
end
end
You can write a spec for this method like this:
it 'writes the value to the cache' do
key = :key
value = 'abc'
cache = double 'cache'
rails = double 'Rails', cache: cache
stub_const('Rails', rails)
expect(cache).to receive(:write).with(key, value)
MyClass.new.cache_value key, value
end
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With