Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Stubbing/mocking global constants in RSpec

Tags:

ruby

rspec

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?

like image 342
Paweł Gościcki Avatar asked Oct 21 '11 10:10

Paweł Gościcki


3 Answers

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 :)

like image 113
David Chelimsky Avatar answered Nov 20 '22 19:11

David Chelimsky


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

like image 23
Moiz Raja Avatar answered Nov 20 '22 18:11

Moiz Raja


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
like image 1
Paweł Gościcki Avatar answered Nov 20 '22 19:11

Paweł Gościcki