my_back_pages

プログラミング学習の記録 Ruby / Rails / FjordBootCamp

【Rails】assert_difference の第一引数に、評価したい式の「文字列」を渡している理由

サマリ

  • 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オブジェクトだったら、それをそのまま使う(今回は文字列なので違う)。
  • 違うのであれば、その式をevalRubyプログラムとして評価する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を使うプログラムに触れられてよかった、ということにしたいと思います。