---
title: Testing and Environment Variables in Ruby
teaser: Learn how to test code using data in environment variables with Climate Control.
tags: web,ruby,rails,testing,rspec,ci
author: Josh Clayton
published_on: 2014-12-03
---

As developers have moved hosting applications to cloud-based solutions like
[AWS] and [Heroku][heroku], a number of patterns for developing and deploying
applications emerged. Perhaps the most well-known set of methodologies is the
[Twelve-Factor App][twelve-factor-app], which outlines areas in which
applications become difficult to maintain and what to do to improve them.

One area which impacts developers directly in the code we write and test is
[application configuration][configuration]. Because we deploy across multiple
environments (e.g. staging and production) with different sets of
configurations for services like Stripe and Segment, and because often these
are critical aspects of our application, we want to ensure things work
correctly.

There are a few routes we can take to test the code using environment variables,
but none are ideal.

## Rely on the values of the environment variables set by our application

Most often, these environment variables are defined either in the `.env` or
`config/environments/test.rb` files. Here is an example:

```ruby
# config/environments/test.rb

ENV["TWILIO_CALLER_ID"] = "+15555551212"

# spec/models/calls/call_initiator_spec.rb
require "spec_helper"

describe Calls::CallInitiator do
  it "creates a new call with the appropriate data" do
    call_creator = double("calls", create: nil)
    initiator = Calls::CallInitiator.new("555-555-1234", call_creator)

    initiator.run

    call_data = {
      from: "+15555551212",
      to: "555-555-1234",
    }
    expect(call_creator).to have_received(:create).with(call_data)
  end
end
```

However, testing against these values introduces
[mystery guests][mystery-guest], as any values we're testing against are
defined outside of the specs themselves.

## Override environment variables on a per-test basis

Another common way to test environment variables is by overriding them on a
per-test basis. This introduces additional complexity by adding additional
setup and teardown steps.

```ruby
# spec/models/calls/call_initiator_spec.rb
require "spec_helper"

describe Calls::CallInitiator do
  it "creates a new call with the appropriate data" do
    cached_twilio_caller_id = ENV["TWILIO_CALLER_ID"]
    ENV["TWILIO_CALLER_ID"] = "+15555551212"

    call_creator = double("calls", create: nil)
    initiator = Calls::CallInitiator.new("555-555-1234", call_creator)

    initiator.run

    call_data = {
      from: "+15555551212",
      to: "555-555-1234",
    }
    expect(call_creator).to have_received(:create).with(call_data)

    ENV["TWILIO_CALLER_ID"] = cached_twilio_caller_id
  end
end
```

Because `ENV` contains global state, and because there are no expectations
about which other tests are relying on this state, we must always cache and
reassign state every time we modify `ENV` for a test.

## Stub or Mock `ENV`

Stubbing or mocking `ENV` is another option (at least at the unit level) which allows us
to control the values. One added benefit is that mocking and stubbing libraries
traditionally handle cleaning up stubs during [the teardown
phase][four-phase-test].

```ruby
# spec/models/calls/call_initiator_spec.rb
require "spec_helper"

describe Calls::CallInitiator do
  it "creates a new call with the appropriate data" do
    allow(ENV).to receive(:[]).with("TWILIO_CALLER_ID").and_return("+15555551212")

    call_creator = double("calls", create: nil)
    initiator = Calls::CallInitiator.new("555-555-1234", call_creator)

    initiator.run

    call_data = {
      from: "+15555551212",
      to: "555-555-1234",
    }
    expect(call_creator).to have_received(:create).with(call_data)
  end
end
```

I've always been a fan of following ["Don't mock what you don't
own"][dont-mock-what-you-dont-own], and in the case of `ENV` (part of Ruby's
core library), we don't own it (even though I'd consider its interface to be
fairly stable).

## Explicitly access keys with `ENV#fetch`

By using `ENV#fetch` instead of `ENV#[]` to retrieve values in the code we'd
be testing, we reduce likelihood of misspellings or mis-configurations.
This _doesn't_ guarantee variables are used correctly.

One example I've
seen firsthand was the same value (admin and support email addresses) assigned
to two environment variables, and misused in the mailer. When the environment
variable was updated on production (after finding the bug), one email was
emailed to the wrong group of people.

## Test Environment Variables with Climate Control

[Climate Control][climate-control] is a gem which handles the above case of
modifying environment variables on a [per-test basis](#override-environment-variables-on-a-per-test-basis).
It avoids mystery guests,
doesn't stub `ENV`, and (with arbitrarily strange strings!) provides a high
level of confidence that the appropriate environment variables are being used
correctly.

It's likely most applicable in unit and integration level tests,
since we'll likely be using [fakes at the acceptance level][faking-segment].

Let's see Climate Control in action:

```ruby
# spec/models/calls/call_initiator_spec.rb
require "spec_helper"

describe Calls::CallInitiator do
  it "creates a new call with the appropriate data" do
    ClimateControl.modify TWILIO_CALLER_ID: "awesome Twilio caller ID" do
      call_creator = double("calls", create: nil)
      initiator = Calls::CallInitiator.new("555-555-1234", call_creator)

      initiator.run

      call_data = {
        from: "awesome Twilio caller ID",
        to: "555-555-1234",
      }
      expect(call_creator).to have_received(:create).with(call_data)
    end
  end
end
```

Overriding environment variables only within the block ensures state is reset
accordingly and environment variable values are immediately obvious to
developers. Because of this, Climate Control fosters moving more configuration into
`ENV` by making it easier to test, resulting in more adherence to the
twelve-factor app methodology.

### Test Environment Variables with Climate Control using RSpec

To use it with RSpec, define theis module in your spec folder:

```ruby
# spec/support/climate_control.rb
module EnvHelper
  def with_modified_env(options, &block)
    ClimateControl.modify(options, &block)
  end
end

RSpec.configure { |config| config.include(EnvHelper) }
```

Then, your tests would read more straightforward by calling
`with_modified_env` when needed:

```ruby
# spec/models/calls/call_initiator_spec.rb
require "spec_helper"

describe Calls::CallInitiator do
  it "creates a new call with the appropriate data" do
    with_modified_env TWILIO_CALLER_ID: "awesome Twilio caller ID" do
      # your tests
    end
  end
end
```

Check out [climate-control] README for more examples, including how
to modify multiple environment variables, and use the library with Threads.

[heroku]: https://www.heroku.com/
[AWS]: https://tbot.io/aws-platform-guide
[twelve-factor-app]: https://12factor.net/
[configuration]: https://12factor.net/config
[mystery-guest]: https://thoughtbot.com/blog/mystery-guest
[CI]: https://thoughtbot.com/blog/tags/ci
[climate-control]: https://github.com/thoughtbot/climate_control
[dont-mock-what-you-dont-own]: https://8thlight.com/insights/thats-not-yours
[faking-segment]: https://thoughtbot.com/blog/segment-io-and-ruby
[four-phase-test]: https://thoughtbot.com/blog/four-phase-test
