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