探検!Changeマッチャ
皆さんこんにちは! 株式会社 MUGENUP 開発部の osada です。 弊社 で使っているフレームワークは Ruby on Rails ということで、 テストで使用する change マッチャ について調べたことを書きたいと思います。 ターゲットは change でテストを書きたい rails 初心者の方です。 かくいう私もrails歴は半年もなく、初心者の皆さんと一緒に勉強出来ればと思います。
ですがその前に、MUGENUP 開発部って何をやっているの?ということで、 簡単にご紹介したいと思います。
MUGENUP とは?
株式会社 MUGENUP は、イラストを必要とする企業さんから発注を受けて、クリエイターさん達とアートディレクターが連携して、イラストを作成しお届けしています。 それらの各工程「受注、工程分割、担当割り当て、イラストの修正や相談」などの全てを MUGENUP WORK STATION というWEBシステムで管理しています。掲示板、画像投稿に変換、リアルタイム通知、スケジュール管理などなど、機能てんこ盛りなWEBシステムです。これのお陰で、日本中、世界中のクリエイターさん達が、いつ、どこにいても一緒に働けるという、クラウドならではの共同作業を実現しています。
開発部では、これら全てを、Ruby on Rails
で開発しています。Rails 3
, Ruby 2.0 (!)
で動いています。
Change Matcher のブロックは2度評価される
さて、そんな MUGENUP では今 change 熱 が流行っています。現在、開発を株式会社KRAYさんにご協力いたただいているのですが、大変勉強になることばかりで、その一つが 良いテストを書く ということです。
綺麗なテストを書くためには、should...==
を羅列するのではなく、change().from().to()
を使おう、ということになるのですが、どう書いたら良いかわからないことが多かったです。
例えば、ユーザーの email を update すると、新しいメールアドレスになる
ということを下記のように書きました。
expect{ put :update, {id: @user.id, user: {email: new_email}} }.to change{@user.reload.email}.from(@user.email).to(new_email)
これとは別に下記の書き方もできると思います。
expect{ put :update, {id: @user.id, user: {email: new_email}} @user.reload }.to change(@user, :email).from(@user.email).to(new_email)
どんな書き方が良いのでしょうか?そもそも、なぜこれが動くのでしょうか?
ドキュメントがないならソースを読めばいいじゃない ということで、ソースを当たることにしました。
ソースは私の環境ではvendor/bundle/ruby/2.0.0/gems/rspec-expectations-2.14.2/lib/rspec/matchers/built_in/change.rb
にありました。
インスタンス化 フェーズ
class Change def initialize(receiver=nil, message=nil, &block) @message = message @value_proc = block || lambda {receiver.__send__(message)} @expected_after = @expected_before = @minimum = @maximum = @expected_delta = nil @eval_before = @eval_after = false end
インスタンス化はここです。ブロックを渡した時はそのままが評価されます。change(@user, :email)
のように、receiver
を渡した時は、lambda
化されて、__send__
で渡されるようです。blockが先に来ていますので、change には blockを渡すのが王道なんでしょうか。
matcher フェーズ
def matches?(event_proc) raise_block_syntax_error if block_given? @actual_before = evaluate_value_proc event_proc.call @actual_after = evaluate_value_proc (!change_expected? || changed?) && matches_before? && matches_after? && matches_expected_delta? && matches_min? && matches_max? end def evaluate_value_proc case val = @value_proc.call when Enumerable, String val.dup else val end end
match 判定 はこれです。インスタンス化で渡されたvalue_proc
が、event_proc
の前後で発動し、評価されていることがわかります。
また、@value_proc
の返り値が Enumerable, String
だった場合には、dup
で返しているのも気が利いていますね。こうしないと破壊的操作がされたときに、追従してしまうことになり、テストが成立しないからだと考えられます。さらに言えばcase ... when
では val
がEnumerable, String
クラスのインスタンスかどうかを判定できることを利用した美しいコードです。
from と to と by と
match 判定の部分を見て行きましょう。
(!change_expected? || changed?) && matches_before? && matches_after? && matches_expected_delta? && matches_min? && matches_max?
changed
は実行前後の値の変化を、change_expected
は by
メソッドが呼ばれているかどうかで判定しています。
def changed? @actual_before != @actual_after end def change_expected? @expected_delta != 0 end def by(expected_delta) @expected_delta = expected_delta self end
matches_before?
は from
メソッドが呼ばれた時に @eval_before
フラグがセットされ、from
に渡した値と、@value_proc
の値が比較されます。
def matches_before? @eval_before ? expected_matches_actual?(@expected_before, @actual_before) : true end def from (before) @eval_before = true @expected_before = before self end
matches_after?
はその逆で、to
メソッドが呼ばれた時に同じ挙動となります。
matches_expected_delta?
は by
メソッドの判定で、実行前の実測値と差分の期待値が、実行後の実測値と同じかどうかを判定していました。
def matches_expected_delta? @expected_delta ? (@actual_before + @expected_delta == @actual_after) : true end
これがおよそ、match 判定の部分です。
from
もto
もby
も、フラグをセットし、実行前後の値の比較という挙動をしています。
およそ change マッチャの挙動を理解できたのではないでしょうか。
シンプルで素晴らしい構造だと思います。
そしてソースを読んだことで、知らなかったメソッドも知ることが出来ました。
それがby_at_least
とby_at_most
です。
by_at_least と by_at_most
by_at_least
とby_at_most
は下記となります。実行後と実行前の差分が、(少なくとも|多くとも) x である、ということのようです。いつ使うんでしょうか?ランダム要素があるときは、Hoge.stub(:rand){}
で確定させて返してしまうのですが、その用途ではないのでしょうか。機会があれば、使ってみたいと思います。
def by_at_least(minimum) @minimum = minimum self end def by_at_most(maximum) @maximum = maximum self end def matches_min? @minimum ? (@actual_after - @actual_before >= @minimum) : true end def matches_max? @maximum ? (@actual_after - @actual_before <= @maximum) : true end
まとめ
さて、長々と、Change class を見てきましたが、初めの疑問、change のブロックの中に reload を書くか、書かないか、はどちらが良いのでしょうか?
changeブロックが2回評価されることを考えると、1度目のreloadは無意味なのにDBアクセスを起こすので、changeの外にだすべきなのかな、と個人的に思います。ただ時間を計測したところそれほど差異が無かったため、もう少し突き詰めたいところです。
あと、今更で大変恐縮なのですが、changeマッチャについて知りたい人は、かの有名な Rubyist Magazine - スはスペックのス 【第 1 回】 RSpec の概要と、RSpec on Rails (モデル編)を読んでください。これだけ読めば、テストが書けるようになります!
では、最後に宣伝を。 MUGENUP では、rails を使いたいエンジニアを募集中です。 無限流開発、ご一緒しませんか?
大きな裁量で自社サービス開発!Rubyエンジニアをウォンテッド! - 株式会社MUGENUPの求人 - Wantedly