RSpec(system spec)で環境変数をスタブしても適用されなくて辛かった話
概要
こんにちは、ツクリンクでエンジニアやっております、てつです。
Railsアプリケーションでは、環境変数を用いて設定値を動的に変更することがよくあります。
最近ではあまり見かけない書き方ですが、弊社ではERBファイル内のJavaScriptコードにRubyで定義された定数を参照している箇所があります。
今回、RSpecでスタブを用いて環境変数を変更しようとしましたが、期待通りに機能しませんでした。その原因と解決策を解説します。
※ 本記事ではsystem specでの例を記載しています。controller specやview specの場合はHelperで書き換える方法など、より良い解決方法があるので事情が変わります。
環境変数の使用方法(サンプルコード)
以下のコードでは、環境変数MY_ENV_VARをSAMPLE定数に代入し、ERBファイル内のJavaScriptでその値を利用しています。
モデルの定義
以下のようにクラスで定数に環境変数を代入しています。
class MyModel < ActiveRecord
SAMPLE = ENV.fetch('MY_ENV_VAR', 15_000)
end
ERBテンプレート
<script>
const sampleVar = <%= MyModel::SAMPLE %>;
console.log("sample var: ", sampleVar);
</script>
RSpecのスタブ設定
テストでは、本番環境とは異なる値(15)を使用して動作を確認したいと考えました。以下のコードで環境変数をスタブしました。
RSpec.describe 'sample', type: :system do
before do
allow(ENV).to receive(:fetch).and_call_original
allow(ENV).to receive(:fetch).with('MY_ENV_VAR', 15_000).and_return(15)
end
it 'test sample' do
visit sample_path
# テスト実行コード(例: ファイルアップロード)
end
end
実際の動作
しかし、テストを実行した結果、ERB内のJavaScriptで参照された値はスタブした値(15)ではなく、デフォルト値(15_000)になっていました。
console.log("sample var: ", sampleVar); // 期待する値は15ですが、15000が出力されました。
原因
定数SAMPLEの評価タイミングが原因でした。
スタブが適用されなかった原因は以下の通りです。
定数の評価タイミング
SAMPLE = ENV.fetch('MY_ENV_VAR', 15_000)
はクラスロード時(RailsやRSpecの起動時)」に一度だけ評価されます。
これが仮にview specであればテンプレート単位でテストされるため、クラスメソッドで環境変数を呼び出したり、コントローラにてインスタンス変数に代入することでスタブの対象をクラスメソッドやインスタンス変数に代替するなどの解決策が考えられます。
しかしsystem specにおいて、erb(assetsに含まれるファイル)内で参照される変数はrailsのプロセス起動時にassetsの内容が確定されるため、RSpecのスコープ内で値をスタブすることができません。
そのため、RSpecのallow(ENV)
でスタブする前に、SAMPLE
の値がすでに固定されています。
クラスロード時にすでに値が確定しているため、ERBテンプレート内では、MyModel::SAMPLEがすでに評価済みの値(15_000)を参照します。
テスト中に環境変数をスタブしても、定数に固定された値は更新されません。
解決策
この問題を解決するために、クラスメソッドやHelperクラスを使用したりと色々試しましたが、結果として、テスト環境のRails起動時に環境変数を直接設定する必要がありました。
スタブの用意が不要であったり、定数を使用している箇所を書き換える必要はありませんが、テストケースごとに異なる値を設定することは困難です。
ENV['MY_ENV_VAR'] = '15'
まとめ
controller specやview specではもっと簡単に値をスタブする方法はありますが、system specではRailsアプリケーション全体を起動してブラウザ操作する点で直接スタブできる範囲が他のタイプのspecよりも少ないようです。
この経験を通じて、テストを見据えたコーディングの重要性を実感しました。特に、起動時に固定される値と遅延評価される値を区別し、controllerやviewなど小さな単位でテスト可能な設計を心がけることで、後々のテストや保守が格段に楽になると学びました。
この記事が同様の問題を抱える方の助けになれば幸いです。
Discussion