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