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