RSpec の let変数が it の block 以外でアクセスできない理由
こんにちは。st-1985 です。
今日はプロジェクトで採用されているRSpecについて、最近遭遇したエラーとその発生理由について、備忘録として書きたいと思います。
let変数 にアクセスできない
エラーは describeのblock内で 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
定義した変数は describe や contextのblockからではなく、it 等のblockからアクセスするように言われているようです。
なぜ let で定義した変数を describe や context の blockから使えないのか
このエラー自体は不具合というわけではないのですが、なぜエラーが発生するのか気になったので調査をする事にしました。
まずは let の動きを確認してみます。
let はインスタンスメソッドを定義している
let は rspec-core の memoized_helpers.rb で定義されています。
コード上の順番は前後しますが、
にてdefine_method を使用して、指定された名前で(実行コンテキストに)インスタンスメソッドを定義しています。
といっても実際の処理は登録しておらず、メモ化をして、存在しない場合はsuperで同名のメソッドを呼び出しています。
デバッグして確認したところ、この時の実行コンテキストはdescribeによって動的に生成された各テスト内容を表現したクラスのようです。
puts self # RSpec::ExampleGroups::AnExampleOfASimpleTest
コードを少し戻って
で実際の処理(=let に渡したblock)をモジュールに登録しています。
our_module は実行コンテキストに追加された let の処理を定義する為のモジュールでした。
puts our_module # RSpec::ExampleGroups::AnExampleOfASimpleTest::LetDefinitions
この事から let はテスト内容のクラスに(メモ化処理をした)インスタンスメソッドを作成していると理解してよさそうです。
itのblock内はインスタンスの、describeのblock内はクラスの実行コンテキスト
エラーメッセージでは let変数へのアクセスは it のblockから行える旨が書かれていました。
let がインスタンスメソッドを定義していることと合わせるとit のblock内の実行コンテキストはインスタンスになりそうです。
実際に確認してみます。
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
予想通り、itの block内の 実行コンテキストはインスタンスであり、ついでにdescribeのblock内ではクラスであることがわかりました。
つまり、冒頭のエラーが発生していた理由は
-
letで指定された内容はインスタンスメソッドとして登録されるが、describeのblock内 の実行コンテキストはクラスであるため、インスタンスメソッドを参照できない
為に起きていたようでした。
感想
以前は「 it が実行部分でそれ以外は準備」のようなふんわりとした理解だったのですが、今回itやdescribeのblock内 の実行コンテキストの違いを把握できたのでテストを書く際の解像度が変わりそうです。
また、エラーが起きた時は、ちょっと調べてみるぐらいの気持ちだったんですが、RSpecの内部について勉強する良い機会となりました。
といってもまだまだ理解ができていない部分が大半だと思うのでまた機会があれば調査してみたいと思います。
余談
記事がまとまらなくなりそうだったので今回は触れていませんが
- RSpecのセットアップと実行の流れ
-
let!が遅延実行をしない為にbeforeに登録している -
shared_examplesとit_behaves_likeの動き - (今回のような)let変数へのアクセスエラーを通知する為に
method_missingとmethod_defined?を組み合わせている
等も読んでいて面白く感じました。と同時になかなか体力を使った気がします。
Discussion