As developers have moved hosting applications to cloud-based solutions like Heroku, a number of patterns for developing and deploying applications emerged. Perhaps the most well-known set of methodologies is the 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. 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.

Most often, these environment variables are defined either in the .env or config/environments/test.rb files. Testing against these values introduces mystery guests, as any values we’re testing against are defined outside of the specs themselves.

# 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

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.

# 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.

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

# 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”, 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).

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.

Climate Control is a gem which handles the above case of modifying 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.

Let’s see it in action:

# 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