🧪

RSpec の let変数が it の block 以外でアクセスできない理由

2024/06/06に公開

こんにちは。st-1985 です。

今日はプロジェクトで採用されているRSpecについて、最近遭遇したエラーとその発生理由について、備忘録として書きたいと思います。

let変数 にアクセスできない

エラーは describeblock内で let で定義した変数にアクセスしようとして発生しました。
以下はエラー再現のための簡易的なコードイメージです。
(※実際には it_behaves_like に引数として渡そうとしてエラーが発生しましたが主題から外れるため変更しています)

RSpec.describe 'An example of a simple test' do
  subject { 42 }
  let(:expected_value) { 42 }

  expected_value
  # 出力されるエラーメッセージ
  # `expected_value` is not available on an example group 
  # (e.g. a `describe` or `context` block).
  # It is only available from within individual examples 
  # (e.g. `it` blocks) or from constructs that
  # run in the scope of an example (e.g. `before`, `let`, etc).

  it do
    expect(subject).to eq(expected_value)
  end
end

定義した変数は describecontextblockからではなく、it 等のblockからアクセスするように言われているようです。

なぜ let で定義した変数を describecontextblockから使えないのか

このエラー自体は不具合というわけではないのですが、なぜエラーが発生するのか気になったので調査をする事にしました。

まずは let の動きを確認してみます。

let はインスタンスメソッドを定義している

letrspec-core の memoized_helpers.rb で定義されています。

コード上の順番は前後しますが、

https://github.com/rspec/rspec-core/blob/c8e7269707ddb1ac45576752a051aa36ddf5fd04/lib/rspec/core/memoized_helpers.rb#L340-L344

にてdefine_method を使用して、指定された名前で(実行コンテキストに)インスタンスメソッドを定義しています。
といっても実際の処理は登録しておらず、メモ化をして、存在しない場合はsuperで同名のメソッドを呼び出しています。

デバッグして確認したところ、この時の実行コンテキストはdescribeによって動的に生成された各テスト内容を表現したクラスのようです。

puts self # RSpec::ExampleGroups::AnExampleOfASimpleTest

コードを少し戻って

https://github.com/rspec/rspec-core/blob/c8e7269707ddb1ac45576752a051aa36ddf5fd04/lib/rspec/core/memoized_helpers.rb#L327

で実際の処理(=let に渡したblock)をモジュールに登録しています。
our_module は実行コンテキストに追加された let の処理を定義する為のモジュールでした。

puts our_module # RSpec::ExampleGroups::AnExampleOfASimpleTest::LetDefinitions

この事から let はテスト内容のクラスに(メモ化処理をした)インスタンスメソッドを作成していると理解してよさそうです。

itblock内はインスタンスの、describeblock内はクラスの実行コンテキスト

エラーメッセージでは let変数へのアクセスは itblockから行える旨が書かれていました。
let がインスタンスメソッドを定義していることと合わせるとitblock内の実行コンテキストはインスタンスになりそうです。
実際に確認してみます。

RSpec.describe 'An example of a simple test' do
  subject { 42 }

  let(:expected_value) { 42 }
  puts self # => RSpec::ExampleGroups::AnExampleOfASimpleTest

  it 'confirms the scope within it block' do
    puts self # => #<RSpec::ExampleGroups::AnExampleOfASimpleTest:0x0000ffff87632428>
    expect(expected_value).to eq(42)
  end
end

予想通り、itblock内の 実行コンテキストはインスタンスであり、ついでにdescribeblock内ではクラスであることがわかりました。

つまり、冒頭のエラーが発生していた理由は

  • let で指定された内容はインスタンスメソッドとして登録されるが、describeblock内 の実行コンテキストはクラスであるため、インスタンスメソッドを参照できない

為に起きていたようでした。

感想

以前は「 it が実行部分でそれ以外は準備」のようなふんわりとした理解だったのですが、今回itdescribeblock内 の実行コンテキストの違いを把握できたのでテストを書く際の解像度が変わりそうです。

また、エラーが起きた時は、ちょっと調べてみるぐらいの気持ちだったんですが、RSpecの内部について勉強する良い機会となりました。

といってもまだまだ理解ができていない部分が大半だと思うのでまた機会があれば調査してみたいと思います。

余談

記事がまとまらなくなりそうだったので今回は触れていませんが

  • RSpecのセットアップと実行の流れ
  • let! が遅延実行をしない為に before に登録している
  • shared_examplesit_behaves_like の動き
  • (今回のような)let変数へのアクセスエラーを通知する為にmethod_missingmethod_defined? を組み合わせている

等も読んでいて面白く感じました。と同時になかなか体力を使った気がします。

SocialPLUS Tech Blog

Discussion