RspecでENVをどうstubするのがよいのか

More than 3 years have passed since last update.


TL;DR


:one: and_call_original使う

allow(ENV).to receive(:[]).and_call_original

allow(ENV).to receive(:[]).with('HOGE').and_return('1')

これがシンプルな対応策


:two: spec実行前にENVを上書き

参照 -> https://github.com/rspec/rspec-rails/issues/1279#issuecomment-70275896


:three: そもそもENVをstubしなければいけない状況を改善する

def hoge?

ENV['HOGE'] == '1'
end
# または
config.hoge = ENV['HOGE']

として

#hogeconfig.hogeをstubする


ENVをstubするときの問題点

# hoge.rb

def hoge
ENV['HUGA'] ||= 1
ENV['HOGE']
end

# hoge_spec.rb
before do
allow(ENV).to receive(:[]).with('HOGE').and_return('1')
end

という実装(すごく適当)になってたとき

Failure/Error: expect(instance.hoge).to eq '1'

ENV received :[] with unexpected arguments
expected: ("HOGE")
got: ("HUGA")
Please stub a default value first if message might be received with other args as well.

のようにspecが落ちる

stubしているENV['HOGE']以外にENV.[]にアクセスしているものがあるとspecが落ちてしまう。これが問題

今回の例ではENV['HUGA']を意図的に呼ぶようにしてるのでいくらでも対処のしようはありそうだけど、

gemやフレームワークの中でENV.[]を呼び出していることもるので対処はしづらい

もちろんENVに限った問題では無いのだけど、環境変数という性質上ENVがこの問題に陥りやすい


横道 ENV.fetch について


ENV.fetchのすすめ

詳しくは -> http://saiya-moebius.hatenablog.com/entry/2014/12/26/135041

[1] pry(main)> ENV['HOGE']

=> nil
[2] pry(main)> ENV.fetch('HOGE')
KeyError: key not found: "HOGE"
from (pry):2:in `fetch`
[3] pry(main)> ENV.fetch('HOGE', '1')
=> "1"

.[]よりは.fecthのほうが便利なことが多い


.[]をstubするのは適切?

詳しくは -> https://github.com/rspec/rspec-rails/issues/1279#issuecomment-70275896

allow(ENV).to receive(:[]).with('HOGE').and_return('1')

では.[]をstubしてるけど、specが実装に密接に関係しすぎてる

- ENV['HOGE']

+ ENV.fetch('HOGE')

と修正したらspecは落ちてしまう

.[]が呼ばれていることをテストしたいわけではないはずなので、

この修正しただけ落ちてしまうspecはあまり良いspecではない


対応策


:one: and_call_original使う

allow(ENV).to receive(:[]).and_call_original

allow(ENV).to receive(:[]).with('HOGE').and_return('1')

一行追加するだけで済むしとてもシンプルな対応策

対応としては少しダサいが、ほとんど場合これでうまくいく

(.[]をstubするのは適切?の問題は残ったままだけど)


:two: spec実行前にENVを上書き

helperを用意する方法

詳しくは -> https://github.com/rspec/rspec-rails/issues/1279#issuecomment-70275896

(以下コピペ)

module EnvHelpers

def with_env_vars(vars)
original = ENV.to_hash
vars.each { |k, v| ENV[k] = v }

begin
yield
ensure
ENV.replace(original)
end
end
end

RSpec.configure do |c|
c.include EnvHelpers
end

it 'does something with the FOO environment variable' do

with_env_vars 'FOO' => 'bar' do
# logic that depends upon ENV['FOO'] goes here
end
end

すこし仰々しい気がするが、うまく動く(はず)

gemとして実装されているやつ(動作は未確認) -> https://github.com/ScrappyAcademy/rock_candy/blob/5695c7b231a44ef97a1be7293197130e8b553931/lib/rock_candy/helpers.rb


:three: そもそもENVをstubしなければいけない状況を改善する

def hoge?

ENV['HOGE'] == '1'
end
# または
config.hoge = ENV['HOGE']

というような感じにして#hoge?config.hogeをstubする

コードも読みやすくなるはずし、この対応ができるとベストだと思う