何がしたいか
Ruby でグローバル空間にメソッドが書かれたファイルを、グローバル空間に出さずにテストしたい。
例
例えば以下のようなディレクトリ構造で、 apps/app1
と apps/app2
は互いに別々のアプリとして起動されるが、specは一箇所でやってしまいたいというとき。(例えば複数の AWS Lambda の lambda_function.rb
を一つのプロジェクトとして管理してる時みたいなことを考えてください)
.
├── spec
│ └── apps_spec.rb
└── apps
├── app1
│ └── main.rb
└── app2
└── main.rb
で、この main.rb
はグローバルな空間にメソッドを置いている。
def main
'hello, app1'
end
def main
'hello, app2'
end
普通にテスト書こうとすると何が起きるか
このとき spec ファイルでふたつの main.rb
を require
してしまうと、後で読み込まれた方で main
メソッドが上書きされる。
require_relative '../apps/app1/main.rb'
RSpec.describe 'app1' do
describe 'main' do
subject { main }
# 実行時は app2 の main メソッドが上書きされてるので 'hello, app2' となる
it { is_expected.to eq 'hello, app1' }
end
end
require_relative '../apps/app2/main.rb'
RSpec.describe 'app2' do
describe 'main' do
subject { main }
it { is_expected.to eq 'hello, app2' }
end
end
解決策: Module の中にメソッドを展開してモジュールメソッドとして実行する
main.rb
にグローバル空間を汚染させずに、 main
メソッドを実行してあげる必要がある。ここは無理やり Module
の中に展開し、モジュールメソッドとして実行してみましょう。
module_eval
の第二引数として file_name
を渡すと file_name の中で require_relative
とかしててもちゃんと解決される。
ただしapp1, app2 で同名のモジュールを require
してた場合上書き問題起きる。難あり。
module AppExecutor
def self.execute(file_name, &block)
Module.new.yield_self do |m|
# file_name を展開して file_name のディレクトリとして eval
m.module_eval(open(file_name).read, file_name)
m.module_eval {
extend self
yield(self)
}
end
end
end
で、テストの方ではモジュールメソッドとして実行させる。
RSpec.describe 'app1' do
describe 'main' do
subject { AppExecutor.execute('src/app1/main.rb') { |context| context.main } }
it { is_expected.to eq 'hello, app1' }
end
end
RSpec.describe 'app2' do
describe 'main' do
subject { AppExecutor.execute('src/app2/main.rb') { |context| context.main } }
it { is_expected.to eq 'hello, app2' }
end
end
かなり無理やり感はあるが動いたので良しということにする。
実行環境を取り出せるようにしたらモックとかもできるのでそっちのほうがいいかも。
ソースコードはこちら