サマリ
assert_difference
の第一引数には評価したい式を入れるけれど、なぜ「文字列」にして入れるのがピンとこなかった。- このメソッドは第一引数に渡した式の文字列を
eval
で実行するProc
オブジェクトを作り、内部でそれをcall
することで評価していた。
ピンとこなかったこと
assert_difference 'Article.count' do post :create, params: { article: {...} } end
ActiveSupport::Testing::Assertions
'Article.count'
の部分について、なぜArticle.count
という式の戻り値をそのまま使わないのだろうか、という疑問。
式の評価結果の値ではなく、文字列を引数にしていることが(プログラミング初心者的には)モヤモヤする感じ……。
上のAPIリファレンスでの当該メソッドの第一仮引数の名前はexpression
。
expression......式……sikiとは……。
Railsの中身を見てみる
assert_difference
の第一引数の式は文字列で!と覚えて進もうかなと思いましたが、気になったのでRailsの中身を見てみました。
def assert_difference(expression, *args, &block) expressions = if expression.is_a?(Hash) message = args[0] expression else difference = args[0] || 1 message = args[1] Array(expression).index_with(difference) end exps = expressions.keys.map { |e| e.respond_to?(:call) ? e : lambda { eval(e, block.binding) } } before = exps.map(&:call) retval = _assert_nothing_raised_or_warn("assert_difference", &block) expressions.zip(exps, before) do |(code, diff), exp, before_value| error = "#{code.inspect} didn't change by #{diff}" error = "#{message}.\n#{error}" if message assert_equal(before_value + diff, exp.call, error) end retval end
ここがポイントだったみたいです。
exps = expressions.keys.map { |e| e.respond_to?(:call) ? e : lambda { eval(e, block.binding) } }
- 第一引数に渡した式の文字列は
expressions.keys
の結果の配列に入っていて、それをループで回す。 - 個々の要素が
call
メソッドを持っているか ->Proc
オブジェクトだったら、それをそのまま使う(今回は文字列なので違う)。 - 違うのであれば、その式を
eval
でRubyプログラムとして評価するProc
オブジェクトをlambda
で作って、それを使う。
内部的には、文字列で渡した式をeval
に渡すことで処理してるよ、ということでした。
なお、assert_difference
で(内部的に)チェックするのは、あくまで「文字列の式を実行するProc
」なので、初めからlambda
式を引数に渡してもOK。
A lambda or a list of lambdas can be passed in and evaluated:
assert_difference ->{ Article.count }, 2 do post :create, params: { article: {...} } end assert_difference [->{ Article.count }, ->{ Post.count }], 2 do post :create, params: { article: {...} } end
(上のAPIガイドより引用)
ちなみに{評価したい式: 期待するdifference}
というハッシュでもOK。ここの評価したい式をlambda
にしてもOK。
こんな手法を取っているメソッドはほかにもたくさんありそうな気がするので、なんとなく流れを確認できてよかったような気がしています。
※ここまで書いてから思ったんですが、よく考えてみると単純に(式の評価結果の)値を引数に取ってしまうと、「(ブロック内の処理が実行されたときの)引数に渡した特定の『式』(値ではなく)の結果の違いをチェックする」という、メソッド本来の意味と一致しないですね……。
eval
を使うプログラムに触れられてよかった、ということにしたいと思います。