Home > database >  RSpec - Best Way to Set Env Variable
RSpec - Best Way to Set Env Variable

Time:06-26

I have class methods on a module and put a class variable and return it on each method

module Cryption
 module Crypt
  class << self
    OFF_ENCRYPT = ENV['IS_OFF']
    
    def encrypt
       OFF_ENCRYPT
    end

    def decrypt
       OFF_ENCRYPT
    end
  end
 end
end

That module created as a gem

enter image description here

That forces me to set environment variable before require the class

ENV["IS_OFF"] = "YES"
require "bundler/setup"
require "cryption"

Cryption::Crypt.encrypt
# result => "YES"

Is there the proper way set the environment variable on rspec, I think I can't set the variable after require the module

spec_helper.rb

require "bundler/setup"
require "cryption"

crypt_off_spec.rb

RSpec.describe "YES" do
  before(:context) do
    ENV['IS_OFF'] = "YES"
  end
  it "encryption off" do
     expect(Cryption::Crypt.encrypt).to eq("YES")
  end
end

crypt_on_spec.rb

RSpec.describe "NO" do
  before(:context) do
    ENV['IS_OFF'] = "NO"
  end
  it "encryption off" do
     expect(Cryption::Crypt.encrypt).to eq("NO")
  end
end

CodePudding user response:

Mutating ENV: Generally an Anti-Pattern Easily Avoided with Some Minimal TDD Refactoring

ENV variables are inherited from Ruby's process environment at startup, and while you can change an ENV variable for the current process and any subprocesses at runtime, you're essentially working against the purpose of ENV variables by treating ENV variables as class or instance variables.

From a testing perspective, if you want to validate that a method or class does the right thing when ENV holds a particular value, you should design your application to create a copy of the ENV values you care about, and mutate those as needed. Alternatively, if you're testing whether specific values are properly picked up, you can fork a process and then change the value within the fork, which will not affect that parent process.

Here's what I'd recommend:

  1. Refactor your class to use the ENV variable as a default.
  2. Refactor your class to enable tests or runtime behavior to override the defaults.
  3. Build tests that ensure ENV["ENCRYPTION_ENABLED"] (or whatever you want to name it) is valid, and can be easily modified within your tests or at runtime.

Example Refactoring and Some Test Skeletons

class Foo

  attr_accessor :encryption_enabled

  # set a default of true (or false if you prefer), or take an
  # optional keyword argument like  encryption_enabled: true 
  # when you initialize an instance of your class
  def initialize
    @encryption_enabled = ENV.fetch "ENCRYPTION_ENABLED", true
  end
end

This will pick up your existing environment value, but you now have an accessor where you can change it as needed for specific tests. For example:

Rspec.describe Foo
  describe "#encryption_enabled" do
    it "sets an instance variable based on ENV['ENCRYPTION_ENABLED']" do
      pending
    end

    it "defaults to 'true' if ENV['ENCRYPTION_ENABLED'] is unset" do
      pending
    end

    it "can be set to 'false' via its accessor" do
      subject.encryption_enabled = false
      expect(subject).to be_false
    end
  end
end

You could also update #new to take a keyword argument to override the default value, or perform other actions. The point is that ENV should provide defaults that don't change often, and your classes and methods should override them when necessary. This provides more flexibility for tests.

The only ENV-related tests you should really need are those that ensure that the ENV values are read and parsed correct, such as when they are unset, invalid, or whatever. Changing runtime behavior based on ENV variables is just an anti-pattern, so this is ripe for refactoring.

CodePudding user response:

Env vars aren't really supposed to be changed at runtime. So, calling ENV['IS_OFF'] = "NO" in your tests isn't really a good idea. The problem is, env vars are a global configuration, so if you change it from one test, the value will still be changed when you run the other test.

The easiest way to do this is probably stub_const, which will temporarily change the value of the constant, e.g. stub_const "Cryption::Crypt", "OFF_ENCRYPT", "false". This is the best option, imo.

But for the sake of completeness, I can show you a way to actually change the environment variable from a specific test. First you would need to change the constant to a method, e.g. def off_encrypt; ENV["OFF_ENCRYPT"]; end. Either that or change the encrypt/decrypt methods to just read the env var directly, e.g. not from a constant.

Then, in your tests, you could do something like this:

around(:each) do |example|
  orig_value = ENV["OFF_ENCRYPT"]
  ENV["OFF_ENCRYPT"] = "true" # set the new value
  example.run
ensure
  ENV["OFF_ENCRYPT"] = orig_value
end

Again, I wouldn't really recommend this, but it is possible.

  • Related