読者です 読者をやめる 読者になる 読者になる

探検!Changeマッチャ

皆さんこんにちは! 株式会社 MUGENUP 開発部の osada です。 弊社 で使っているフレームワークRuby on Rails ということで、 テストで使用する change マッチャ について調べたことを書きたいと思います。 ターゲットは change でテストを書きたい rails 初心者の方です。 かくいう私もrails歴は半年もなく、初心者の皆さんと一緒に勉強出来ればと思います。

ですがその前に、MUGENUP 開発部って何をやっているの?ということで、 簡単にご紹介したいと思います。

MUGENUP とは?

株式会社 MUGENUP は、イラストを必要とする企業さんから発注を受けて、クリエイターさん達とアートディレクターが連携して、イラストを作成しお届けしています。 それらの各工程「受注、工程分割、担当割り当て、イラストの修正や相談」などの全てを MUGENUP WORK STATION というWEBシステムで管理しています。掲示板、画像投稿に変換、リアルタイム通知、スケジュール管理などなど、機能てんこ盛りなWEBシステムです。これのお陰で、日本中、世界中のクリエイターさん達が、いつ、どこにいても一緒に働けるという、クラウドならではの共同作業を実現しています。

f:id:mgnup:20131117233603p:plain

開発部では、これら全てを、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では valEnumerable, Stringクラスのインスタンスかどうかを判定できることを利用した美しいコードです。

from と to と by と

match 判定の部分を見て行きましょう。

          (!change_expected? || changed?) && matches_before? && matches_after? && matches_expected_delta? && matches_min? && matches_max?

changedは実行前後の値の変化を、change_expectedby メソッドが呼ばれているかどうかで判定しています。

        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 判定の部分です。 fromtobyも、フラグをセットし、実行前後の値の比較という挙動をしています。 およそ change マッチャの挙動を理解できたのではないでしょうか。 シンプルで素晴らしい構造だと思います。

そしてソースを読んだことで、知らなかったメソッドも知ることが出来ました。 それがby_at_leastby_at_mostです。

by_at_least と by_at_most

by_at_leastby_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

f:id:mgnup:20131118023234p:plain