概要
- Rubyのinstance_execは、レシーバのインスタンス内のコンテキストで実行されるが、ローカル変数についてはブロックの外側と共有される
背景
-
最近Rubyな会社に入ってRSpecでテストを書き始めたのだが、itに変数を渡す際に挙動がわからずはまった
-
具体的には以下なクラスがあったとする
class Sample def hello return "hello" end end
-
普通にテストを書くと以下な感じになると思う
describe Sample do before do @sample = Sample.new end it { expect(@sample.hello).to eq("hello") } end
-
が、Rspecを理解してなかったので当初は以下な感じで書こうとして、
undefined method ``hello' for nil:NilClass
とエラーになっていた。describe Sample do @sample = Sample.new it { expect(@sample.hello).to eq("hello") } end
-
上記に悩みつつ、以下な感じで書いたらエラーにならなかった。
describe Sample do sample = Sample.new it { expect(sample.hello).to eq("hello") } end
-
RSpecのソースコードやこの記事を読んで、
@sample
のときにエラーになる原因はわかった- describe内はRSpec::Core::ExampleGroupのサブクラスであり、it内はそのインスタンスである
- 内部的にはインスタンスをレシーバーとしてinstance_execで実行されている
- describe内の記述は、上記サブクラスに対するmodule_execによりサブクラスの定義式として実行されるため、
@sample
はExampleGroupクラス自身のインスタンス変数(クラスインスタンス変数)になる - その結果、it内では
@sample
が参照できず、nilになる
- describe内はRSpec::Core::ExampleGroupのサブクラスであり、it内はそのインスタンスである
-
が、ローカル変数の場合になぜうまく動作するのかわからなかった
結果
-
instance_exec(instance_evalでも同じく)は、ローカル変数についてはブロックの外側と共有される
irb(main):001:0> class Sample irb(main):002:1> def initialize irb(main):003:2> @hoge = "hoge" irb(main):004:2> end irb(main):005:1> end irb(main):006:0> fuga = "fuga" irb(main):007:0> @hoge = "hogehoge" irb(main):008:0> Sample.new.instance_exec { "hoge: #{@hoge}, fuga: #{fuga}" } => "hoge: hoge, fuga: fuga"
-
よって、ローカル変数の場合はbeforeで渡さなくてもうまく動作した
- とはいえ、ぱっと見でわかりづらいので、RSpec的にはbeforeでインスタンス変数を渡すか、letを使うのがよいのかな