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
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:
- Refactor your class to use the ENV variable as a default.
- Refactor your class to enable tests or runtime behavior to override the defaults.
- 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.