RSpecのletを動的に使用する

RSpecで let を動的に定義・使用するようにして shared_context を使いまわしたかったのでメモ。

まずはletがどのように実装されているか確認する

letの定義 を見てみると、

def let(name, &block)
  ︙
  # Apply the memoization. The method has been defined in an ancestor
  # module so we can use `super` here to get the value.
  if block.arity == 1
    define_method(name) { __memoized.fetch_or_store(name) { super(RSpec.current_example, &nil) } }
  else
    define_method(name) { __memoized.fetch_or_store(name) { super(&nil) } }
  end
end

上記のように define_method が使われており、 let は一見変数を定義しているようですが実際は関数を定義していました。

結論

なので send を使えばPHPでいう可変変数のように呼び出すことができます。

shared_context 'foobar' do |label|
  let("#{label}_y") { rand(-10..10) }
  let("#{label}_x") { rand(-10..10) }
  let("#{label}_result") { Math.atan2(send("#{label}_y"), send("#{label}_x")) }
end

# 使用例は以下のような感じ
context 'hogefuga' do
  include_context 'foobar', 'hoge' # hoge_y, hoge_x, hoge_result が定義される
  include_context 'foobar', 'fuga' # fuga_y, fuga_x, fuga_result が定義される

  it { expect(hoge_result).to be_between(-Math::PI, Math::PI).inclusive }
  # it内でもsendでletを動的に呼べる
  it { expect(send('fuga_result')).to be_between(-Math::PI, Math::PI).inclusive }
end