見出し画像

rspecのPredicate matchersの実装を読んでみる

こんにちは!
bosyu Advent Calendar 2019の 22日目の記事です。

最近bosyu社内で、メタプログラミングRuby 第2版の輪読会を週1回お昼に開催していますが、久しぶりに読んだので色々思い出すことがあって楽しいです😊
メタプログラミングはとても便利なのですが、やりすぎると何かあった場合にコードを追いにくくなるので使い所が難しいですね!
ということで実際にどんな感じで使われているかrspecのコードを読んでみます。

Predicate matchers

rspecにはPredicate matchersというものがあります。
簡単に説明すると xxx? というメソッドがある場合には be_xxx というmatcherが、has_xxx というメソッドがある場合は have_xxx というmatcherが使えます。

# これが
expect(0.zero?).to be_truthy
# こう書ける
expect(0).to be_zero

# これが
expect({ a: 1, b: 2 }.has_key?(:a)).to be_truthy
# こう書ける
expect({ a: 1, b: 2 }).to have_key(:a)

今回はこれの be_xxx の動作について見てみようと思います。

be_xxxの動作を見てみる

試しに be_zero を呼んでみると RSpec::Matchers::BuiltIn::BePredicate が返ってきます。

pry> p be_zero
#<RSpec::Matchers::BuiltIn::BePredicate:0x0007fb938fdf860 @prefix="be_", @expected="zero", @args=[], @block=nil>

RSpec::Matchers::BuiltIn::BePredicate はどこで生成されているかというと
https://github.com/rspec/rspec-expectations/blob/master/lib/rspec/matchers.rb#L954-L967 で生成されていました。

BE_PREDICATE_REGEX = /^(be_(?:an?_)?)(.*)/
HAS_REGEX = /^(?:have_)(.*)/
DYNAMIC_MATCHER_REGEX = Regexp.union(BE_PREDICATE_REGEX, HAS_REGEX)
def method_missing(method, *args, &block)
  case method.to_s
  when BE_PREDICATE_REGEX
    BuiltIn::BePredicate.new(method, *args, &block)
  when HAS_REGEX
    BuiltIn::Has.new(method, *args, &block)
  else
    super
  end
end

be_zero メソッドは定義されていないため、 method_missing メソッドが呼び出されます。
そして method_missing メソッドの第1引数には呼び出したメソッド名のシンボルが入っているので、BE_PREDICATE_REGEX とマッチし、 BuiltIn::BePredicate.new(method, *args, &block) が実行されます。

実際にマッチしているかどうかの判定は
https://github.com/rspec/rspec-expectations/blob/master/lib/rspec/matchers/built_in/be.rb#L181
に実装があります。
関係ある場所を抜粋すると以下のような感じになります。

def matches?(actual, &block)
 @actual  = actual
 @block ||= block
 predicate_accessible? && predicate_matches?
end

def predicate
 :"#{@expected}?"
end

def predicate_matches?
 method_name = actual.respond_to?(predicate) ? predicate : present_tense_predicate
 @predicate_matches = actual.__send__(method_name, *@args, &@block)
end

expect(0).to be_zero を実行した場合には actual には 0@expected には zero が入っています。
なので actual.__send__(method_name, *@args, &@block) 0.__send__(:zero?, *@args, &@block) となり、 true となるので expect(0).to be_zero はOKになります。

感想

Predicate matchersは method_missingsend を使うことで、予めメソッドを定義せずに、色々なケースに対応することができるようになっていました。specで使われていて、様々なメソッドを持つオブジェクトが入ってくる可能性があるのでメタプロの使い所としては正しいなぁと感じました。
メタプロは楽しいのでやっていきたい!ので何かgemでも作ろうかな〜

この記事が気に入ったらサポートをしてみませんか?