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

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.

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

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.

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

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

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.

I'd avoid ENV.stub(:[]) - it does not work if other things are using ENV such as pry(you'll get an error about needing to stub DISABLE_PRY).
#stub_const works well as already pointed out.

You can use https://github.com/littleowllabs/stub_env to achieve this. It allows you to stub individual environment variables without stubbing all of them as your solution suggested.
Install the gem then write
before :each do
stub_env('AWS_ACCESS_KEY_ID', 'asdf')
stub_env('AWS_SECRET_ACCESS_KEY','secret')
end

What you want is the dotenv gem.
Running tests under foreman, as #ciastek suggests, works great when running specs from CLI. But that doesn't help me run specs with Ruby Test in Sublime Text 2. Dotenv does exactly what you, transparently.

Related

Testing Ruby-based CLIs with Aruba and Bundler

I have an RSpec suite, run via Bundler, that is testing a number of different command-line applications using Aruba. It works fine ... as long as the command being tested is not itself written in Ruby using Bundler. But I cannot figure out how to prevent the RSpec suite's bundler config from interfering with the execution of commands that themselves use Bundler - at least, not without extreme measures.
I have tried various permutations of unset_bundler_env_vars and with_clean_env, to no avail. Here's an example of a technique I thought would work:
describe 'my ruby app' do
before :each { unset_bundler_env_vars }
it 'should work' do
Bundler.with_clean_env { run_simple ruby_command_name }
end
end
I also tried unset_bundler_env_vars without with_clean_env, and vice-versa, in case they interfered with each other. No dice.
The only way I've gotten it to work is to massage Aruba's copy of the environment manually, like this:
before :all do
aruba.environment.tap do |env|
if env.include? 'BUNDLE_ORIG_PATH' then
env['PATH'] = env['BUNDLE_ORIG_PATH']
%w(BUNDLE_BIN_PATH BUNDLE_GEMFILE BUNDLE_ORIG_PATH GEM_HOME RBENV_DIR
RBENV_HOOK_PATH RUBYLIB RUBYOPT).each do |key|
env.delete key
end
end
end
end
There must be a better way. Neither the test suite nor the command being tested should know or care what language the other is written in. And my test code that uses Aruba and Bundler should not need to know the details of how bundle exec affects the process environment.
So what am I doing wrong? How should I be doing this?
It looks like unset_bundler_env_vars is deprecated and replaced by delete_by_environment_variable which requires a string param (source).
You might try before :each { delete_environment_variable('BUNDLE_GEMFILE') } in your spec. If that does not work, you may need to iterate through the PATH variable list to delete each one.
In the deprecation notice, there is a work-around, though I am not sure how brittle that would be moving forward.
unset_bundler_env_vars
aruba.environment.clear.update(ENV)
Hope this helps.

Running Cucumber tests on different environments

I'm using Cucumber and Capybara for my automated front end tests.
I have two environments that I would like to run my tests on. One is a staging environment, and the other is the production environment.
Currently, I have my tests written to access staging directly.
visit('https://staging.somewhere.com')
I would like to re-use the tests in production (https://production.somewhere.com).
Would it be possible to store the URL in a variable in my step definitions
visit(domain)
and define domain using an environment variable called form the command line? Like
$> bundle exec cucumber features DOMAIN=staging
if I want to point the tests to my staging environment, or
$> bundle exec cucumber features DOMAIN=production
if I want it to run in production?
How do I go about setting this up? I'm fairly new to Ruby and I've been searching the forums for a straight forward information but could not find any. Let me know if I can provide more information. Thanks for your help!
In the project's config file, create a config.yml file
---
staging:
:url: https://staging.somewhere.com
production:
:url: https://production.somewhere.com
Then extra colon in the yml file allows the hash key to be called as a symbol.
In your support/env.rb file, add the following
require 'yaml'
ENV['TEST_ENV'] ||= 'staging'
project_root = File.expand_path('../..', __FILE__)
$BASE_URL = YAML.load_file(project_root + "/config/config.yml")[ENV['TEST_ENV']][:url]
This will default to the staging environment unless you override the TEST_ENV. Then, from your step or hook, you can call:
visit($BASE_URL)
or you might need :/
visit "#{$BASE_URL}"
This will allow you to use
bundle exec cucumber features TEST_ENV=production
I don't use cucumber much but you should be able to do
bundle exec cucumber features DOMAIN=staging
then in your tests use ENV['DOMAIN'] || YOUR_DEFAULT_DOMAIN to utilize this variable. YOUR_DEFAULT_DOMAIN should probably be your test environment.
See Here

Detect that code run by RSpec, Ruby

I would like to be able to know that my code is run under rspec or not. Is this possible?
The reason is that I am loading some error loggers that would be cluttered with deliberate errors (expect{x}.to raise_error) during testing.
I have looked at my ENV variable, and there is no (apparent) signs of a test environment variable.
Add at the beginning of your spec_helper.rb:
ENV['RACK_ENV'] = 'test'
Now you can check in your code whether the RACK_ENV is test or not.

Using minitest-reporters with TeamCity (minitest-reporters tries to override reporter selection)

Does anyone have experience with using minitest-reporters for correct test output on TeamCity?
I want to use the JUnit reporter and when I am running the rake test task on my computer, the output is correctly in xml format under test/reports. But when I run the tests from TeamCity, then it tries to use the RubyMine reporter instead (although test_helper specifies JUnit reporter). I found that reports.rb file from mintiest-reporters has these lines:
def self.choose_reporters(console_reporters, env)
if env["TM_PID"]
[RubyMateReporter.new]
elsif env["RM_INFO"] || env["TEAMCITY_VERSION"]
[RubyMineReporter.new]
else
Array(console_reporters)
end
end
And it seems that here the RubiMine reporter is chosen by default when these environment variables are given, although I specified another reprter in my test_helper (and it seems that I could not use the RubiMine reporter as it tries to require some TeamCity files and they somehow cannot be accessed).
I tried to override TEAMCITY_VERSION while calling rake test but it does not seem to work.
Has anyone solved this problem? Or is there some other reporter gem that would work better (I also tried ci_reporter, as it works with my other projects with lower ruby versions but somehow not with 2.0.0)?
I had this exact problem today and was able to solve it by unsetting the 3 environment variables before executing the rake task.
For example, I have a TeamCity command line build step that basically looks like this:
#! /bin/bash
unset TM_PID
unset RM_INFO
unset TEAMCITY_VERSION
rake my_task

What is the best way to store project specific config info in ruby Rake tasks?

I have rake tasks for getting the production database from the remote server, etc. It's always the same tasks but the server info changes per project. I have the code here: https://gist.github.com/868423 In the last task, I'm getting a #local_db_dir_path = nil error.
I don't think want to use shell environment variables because I don't want to set them up each time I use rake or open a new shell.
Stick the settings in a YAML file, and read it like this:
require 'yaml'
config = YAML.load("config.yaml") # or wherever
$remote_host = config['remote_host']
$ssh_username = config['ssh_username']
# and so on
Or you can just read one big config hash:
$config = YAML.load("config.yaml")
Note that I'm using globals here, not instance variables, so there's no chance of being surprised by variable scope.
config.yaml would then look like this:
---
remote_host: some.host.name
ssh_username: myusername
other_setting: foo
whatever: bar
I tend to keep a config.yaml.sample checked in with the main body of the code which has example but non-working settings for everything which I can copy across to the non-versioned config.yaml. Some people like to keep their config.yaml checked in to a live branch on the server itself, so that it's versioned, but I've never bothered with that.
you should be using capistrano for this, you could use mulitsage or just separate host setting to a task, example capistrano would look like this:
task :development do
server "development.host"
end
task :backup do
run "cd #{current_path}; rake db:dump"
download "remote_path", "local_path"
end
and call it like this:
cap development backup

Resources