What is the best way to write specs for code that depends on environment variables?

24,172

Solution 1

That would work.

Another way would be to put a layer of indirection between your code and the environment variables, like some sort of configuration object that's easy to mock.

Solution 2

You also can stub the constant:

stub_const('ENV', {'AWS_ACCESS_KEY_ID' => 'asdf'})

Or, if you still want the rest of the ENV:

stub_const('ENV', ENV.to_hash.merge('AWS_ACCESS_KEY_ID' => 'asdf'))

Solution 3

This syntax works for me:

module SetEnvVariable

  def set_env_var(name, value)
   # Old Syntax
   # ENV.stub(:[])
   # ENV.stub(:[]).with(name).and_return(value)

   allow(ENV).to receive(:[]) # stub a default value first if message might be received with other args as well.
   allow(ENV).to receive(:[]).with(name).and_return(value)
  end

end

Solution 4

As Heroku suggests, you can use Foreman's .env file to store environment variables for development.

If you do that, you can use foreman run to run your specs:

foreman run bundle exec rspec spec

Solution 5

If you're using dotenv to setup your environment during tests but need to modify an env variable for a specific test then following approach can be useful.

A simpler method than stubbing ENV is to replace the environment for the duration of the test, and then restore it afterwards like so:

with_environment("FOO" => "baz") do
  puts ENV.fetch("FOO")
end

Using a helper like this:

module EnvironmentHelper
  def with_environment(replacement_env)
    original_env = ENV.to_hash
    ENV.update(replacement_env)

    yield
  ensure
    ENV.replace(original_env)
  end
end

By using ensure the original environment is restored even if the test fails.

There's a handy comparison of methods for setting & modifying environment variables during tests including stubbing the ENV, replacing values before / after the test, and gems like ClimateControl.

Share:
24,172
Luke Francl
Author by

Luke Francl

I am a software developer currently living in San Francisco. My speciality is web-based applications, lately using Ruby on Rails, though I have probably written more web applications in Tcl than most people. Check out my CV if you're looking for a software engineer.

Updated on July 05, 2022

Comments

  • Luke Francl
    Luke Francl almost 2 years

    I am testing some code that pulls its configuration from environment variables (set by Heroku config vars in production, for local development I use foreman).

    What's the best way to test this kind of code with RSpec?

    I came up with this:

    before :each do
        ENV.stub(:[]).with("AWS_ACCESS_KEY_ID").and_return("asdf")
        ENV.stub(:[]).with("AWS_SECRET_ACCESS_KEY").and_return("secret")
    end
    

    If you don't need to test different values of the environment variables, I guess you could set them in spec_helper instead.

  • Andrew Marshall
    Andrew Marshall about 12 years
    If this is in Rails you already have its own configuration object to add to if you want.
  • Luke Francl
    Luke Francl about 12 years
    That is a handy tip. I did not know you could define your own configuration options so easily. Just do this in environments/*.rb: config.my_config_value = 'value' and it will be made available as Rails.configuration.my_config_value.
  • David Tuite
    David Tuite over 9 years
    ENV doesn't appear to have a merge method (at least in Ruby 1.9.3).
  • Fabio
    Fabio over 9 years
    This is the best approach for a quick env stubs, but in second case you need ENV.to_hash to preserve original env.
  • Tadas Sasnauskas
    Tadas Sasnauskas almost 9 years
    It does not help if you want to test outcomes of different values in the same env variable.
  • Pratik Khadloya
    Pratik Khadloya over 8 years
    Making the hash keys double quoted made it work for me. When i had the keys with single quotes, they got converted to :symbols. Ex: :AWS_ACCESS_KEY_ID
  • hoffmanc
    hoffmanc over 5 years
    won't work if you use e.g., fetch for ENV access.
  • Elliott de Launay
    Elliott de Launay over 2 years
    add a couple more lines for fetch: allow(ENV).to receive(:fetch).with(name).and_return(value)
  • Richard-Degenne
    Richard-Degenne about 2 years
    This is probably the cleanest answer I've seen here. I'd use it with an around block for it to be completely idiomatic.