should を捨て expect を使うようになった、たった一つの理由
皆さんこんにちは。スーパーのビニール袋は濡れタオルを触ってから開ける方、MUGENUP の osada です。
今回はexpect
について調査したことを書きたいと思います。
ターゲットは、rspec
が上手く書けなくて、気が付くと3時間もテストを書いている、というようなrspec
初心者です(実話)。
中間管理職 expect さん
では、本題のexpect
です。まずは問1から。5秒で答えてください。
describe Person do it "should be a instance" do expect{Person.new}.to be_a(Person) end it "should be a instance" do expect(Person.new).to be_a(Person) end end
問1
- 両方テストが通る
- 上のテストが通らない
- 下のテストが通らない
- 両方通らない
できました?正解は
解答: 2. 上のテストが通らない
1) Person should be a instance Failure/Error: expect{Person.new}.to be_a Person expected #<Proc:0x007fbcf91a75a8@/Users/osada/projects/ruby_test/rspec_test/spec/person_spec.rb:9> to be a kind of Person # ./spec/person_spec.rb:9:in `block (2 levels) in <top (required)>'
では、問2
上のテストが通らない理由を説明してください
違いは、{}
と()
です。しかし前回のchange マッチャで使っていたのは{}
のブロックでした。今回はなぜダメなんでしょうか?
調べていきましょう。
ソースと共にあらんことを
ソースコードを読んで分かるように書くべきである、という考え方にあるように、Rubyist は常にソースと共にあるようです。 では、expect の深淵に足を踏み入れたいと思います。
私の環境では、ruby/2.0.0/gems/rspec-expectations-2.14.2/lib/rspec/expectations/syntax.rb
が入り口のようです。
expect と to
def expect(*target, &target_block) target << target_block if block_given? raise ArgumentError.new("You must pass an argument or a block to #expect but not both.") unless target.size == 1 ::RSpec::Expectations::ExpectationTarget.new(target.first) end
block
のときは、target
に追加してから、target.first
を委譲しています。ということは、引数とブロックを同時に渡せるのか?、とおもいきや、target.size==1
以外受け付けないので、ちょっと不思議な構造です。
::RSpec::Expectations::ExpectationTarget
は同じ階層のexpectation_target.rb
にあります。
# @example # expect(something) # => ExpectationTarget wrapping something class ExpectationTarget # @api private def initialize(target) @target = target end
とあるように、expect
はただのラッパーのようです。
そして target
はそのまま @target
に代入されています。ブロックだからといって、特別扱いはしていませんね。
では、続いて to
を見てみます。
def to(matcher=nil, message=nil, &block) prevent_operator_matchers(:to, matcher) RSpec::Expectations::PositiveExpectationHandler.handle_matcher(@target, matcher, message, &block) end
引数を見て驚きました。to
にはmessage
とblock
が渡せるのですね。そしてその先は、PositiveExpectationHandler
に委譲しているようです。
なんと expect
がしているのはこれだけです!シンプルですね。
この先はhandler
を読まなければなりません。
PositiveExpectationHandler
PositiveExpectationHandler があるのは、同じ階層の handler.rb
です。
ruby/2.0.0/gems/rspec-expectations-2.14.2/lib/rspec/expectations/handler.rb
class PositiveExpectationHandler < ExpectationHandler def self.handle_matcher(actual, matcher, message=nil, &block) check_message(message) ::RSpec::Matchers.last_should = :should ::RSpec::Matchers.last_matcher = matcher return ::RSpec::Matchers::BuiltIn::PositiveOperatorMatcher.new(actual) if matcher.nil?
いきなり困りました。 コロン2つで始まる式なんてありましたっけ? そのことについては後述することにして先に進めます。
match = matcher.matches?(actual, &block) return match if match
ここが判定の部分です。
前回 change
マッチャ で見ましたが、matcher
の matches?
メソッドを呼びだしています。
こうゆうの(↓)ですね。 to
からくる block
を渡せる matcher
と 渡せない matcher
があるようです。change
は渡せない方だったのですね。
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
結局のところ、expect
で渡したtarget
はマッチャーの所まで、何の加工もなくそのまま委譲されるということが分かりますね。そしてbe_a
マッチャ(実体はbe_kind_of
マッチャ) は、こんな感じです。
class BeAKindOf < BaseMatcher def match(expected, actual) actual.kind_of? expected end end
やっと{}
で書くと失敗する理由がわかった気がします。
つまり、{Person.new}.kind_of? Person
は Proc.kind_of? Person
ですので、Proc
オブジェクトはPerson
のインスタンスではない、といことで失敗しているのでした。===
で評価しているマッチャであれば、成功するに違いありません。
ということで、expect に何を渡すかは、マッチャによるという身も蓋も無い話になってしまいましたが、ここを意識して書いていくことで、もっとスラスラと rspec が書けると良いなと思っています。
message は失敗したときの表示
せっかくなので続きを見ていきましょう。
match で true になったら、return するので、以降は match しなかった時の処理のようです。
message = message.call if message.respond_to?(:call) message ||= matcher.respond_to?(:failure_message_for_should) ? matcher.failure_message_for_should : matcher.failure_message
to
の第2引数、message
はここで使われるようです。
オリジナルのエラーメッセージを返すことができるんですね。試してみます。
it "should be a instance" do expect{Person.new}.to be_a(Person), "通らないヨーダ" end
1) Person should be a instance Failure/Error: expect{Person.new}.to be_a(Person), "通らないヨーダ" 通らないヨーダ # ./spec/person_spec.rb:10:in `block (2 levels) in <top (required)>'
おお、本当に変わりました。
指定しないときはいつものエラーメッセージですが、 それらは matcher が返していたようです。
message.call
はMethod
クラスのことのようですが、奥が深いのでスルーします(えっ)。
エラーメッセージを返すメソッドを作成して、
それを rspec の引数に渡すような使い方を想定しているのかもしれません。
diffable
ここで最後です。diffable?
とは何でしょうか?
if matcher.respond_to?(:diffable?) && matcher.diffable? ::RSpec::Expectations.fail_with message, matcher.expected, matcher.actual else ::RSpec::Expectations.fail_with message end end end
これを持っている matcher を探してみると、base_matcher.rb
で false
,
eq.rb, eql.rb, equal.rb
が true
、およびinclude.rb
はメソッド呼び出しがありました。
> rspec-expectations-2.14.2/lib/rspec/matchers/built_in/base_matcher.rb def diffable? false end > rspec-expectations-2.14.2/lib/rspec/matchers/built_in/eq.rb def diffable?; true; end > rspec-expectations-2.14.2/lib/rspec/matchers/built_in/include.rb def diffable? # Matchers do not diff well, since diff uses their inspect # output, which includes their instance variables and such. @expected.none? { |e| RSpec::Matchers.is_a_matcher?(e) } end
比較可能な場合において、期待値と実測値を表示するのに使うようですね。
まとめ
今回の調査でわかったのは下記の2点です。
expect
はmatcher
にそのまま委譲する。expect{}.to
で書けるのは、matcher 側がブロックを評価しているときのみto
にはmessage
と&block
が渡せる
ということで、matcher を知ることが、expect を書けるようになる条件なんですね。 基本の matcher はBuilt in matchers - RSpec Expectations - RSpec - Relishにありましたので、勉強していきたいと思います。
誤差をOKにするbe_with_in
、範囲を比較する cover
、他にも色々あるので、便利に使っていきたいです。
初めて知ったマッチャについてご紹介だけしておきます。
be_with_in
誤差をOKにするマッチャshould be_within(0.5).of(27.9)
exist
exist? を判定するマッチャ。expect(obj).to exist
match
正規表現のマッチャ=~
と同じexpect("a string").to match(/str/)
respond_to
クラスのメソッドが呼び出せるかどうか。しかも引数の呼び出し個数までマッチできる!expect([1, 2, 3]).to respond_to(:take).with(1).argument
satisfy
ブロックが渡せるマッチャ。何でもできちゃう。expect(10).to satisfy { |v| v % 5 == 0 }
throw
throw で渡ってきたシンボルを確認するマッチャexpect { throw :foo }.to throw_symbol(:foo)
yield
一言では説明できないのでパス! yield matchers - Built in matchers - RSpec Expectations - RSpec - Relishcover
範囲をマッチ。というか、range に cover なんてメソッドあったんですね。expect(1..10).to cover(5)
- start_with, end_with。先頭か末尾をマッチ。というか、ファイル名が
start_and_end_with.rb
なんですがexpect("this string").to end_with "string"
使えそうなもの、使えなさそうなもの。色々あって、楽しいですね。 これからも良いrspecを書いていきたいと思います。
あ、表題のshould
を捨てた理由、が書いてありませんでしたね。
should
がself
を対象にしたhandler
呼び出しでしかないから。
syntax_host.module_eval do def should(matcher=nil, message=nil, &block) ::RSpec::Expectations::PositiveExpectationHandler.handle_matcher(self, matcher, message, &block) end
やってること同じじゃん、ということで、これなら expect で書いたほうがクラスを開く必要がない分が良いな、と思った次第です。文字数増えるのはイヤなんですけどね〜。
では皆さん。良いspec ライフをお送り下さい。
宣伝
MUGENUP では、rails を使いたいエンジニアを募集中です。 無限流開発、ご一緒しませんか?
大きな裁量で自社サービス開発!Rubyエンジニアをウォンテッド! - 株式会社MUGENUPの求人 - Wantedly
(補講) ::って何よ?
さて、ちょっと後に回していた、::
についての調査結果です。
::RSpec::Matchers.last_should = :should
あるクラスまたはモジュールで定義された定数を外部から参照する ためには::演算子を用います。 またObjectクラスで 定義されている定数(トップレベルの定数と言う)を確実に参照する ためには左辺無しの::演算子が使えます。 親クラスとネストの外側のクラスで同名の定数が定義されているとネストの外 側の定数の方を先に参照します。 http://docs.ruby-lang.org/ja/2.0.0/doc/spec=2fvariables.html#const
# -*- coding: utf-8 -*- CONSTANT = "トップの定数だよ" class Person CONSTANT = "内部の定数だよ" def top_level_constant ::CONSTANT end def my_class_constant CONSTANT end end
describe Person do it do expect(Person.new.my_class_constant).to eq "内部の定数だよ" end it do expect(Person.new.top_level_constant).to eq "トップの定数だよ" end end
(補講2) ボクの考えた最強のマッチャ
最後に皆さんに課題です。3秒で選んでください。
it "should be true" do expect{Person.new}.to be_true end
- 通る
- 通らない