RSpec の have_attributes マッチャがリテラルとマッチャを両方指定できる仕組み

have_attributes マッチャ

RSpechave_attributes というマッチャがある。

it { expect(10.to_s).to eq("10") }
it { expect(10.positive?).to eq(true) }

と書くところを

it { expect(10).to have_attributes(to_s: "10", positive?: true) }

と書くことができる。

一つのインスタンスに対して複数の属性をチェックする時に読みやすくできるというメリットがある。

have_attributes の不思議

実は have_attributes

it { expect(10.positive).to be_truthy }

it { expect(10).to have_attributes(positive?: be_truthy) }

と書くことができる。 ハッシュのバリューに「マッチャ」を書くことができるのだ。 つまり have_attributes はハッシュのバリューに「マッチャ」と「リテラル」の両方を書くことができる。 これは結構不思議だと思ったので、RSpecソースコードを読んでみた。

ソースコードを読む

rspec-expectations/have_attributes.rb at 14faeab88f319ac0c2e4d793ec02c2b69eb52a5c · rspec/rspec-expectations · GitHub

この中でハッシュのキーとバリューはそれぞれ value_match? で比較される。

def actual_has_attribute?(attribute_key, attribute_value)
  values_match?(attribute_value, @values.fetch(attribute_key))
end

rspec-expectations/composable.rb at 14faeab88f319ac0c2e4d793ec02c2b69eb52a5c · rspec/rspec-expectations · GitHub

value_match? メソッドは Support::FuzzyMatcher.values_match? を呼び出す。

def values_match?(expected, actual)
  expected = with_matchers_cloned(expected)
  Support::FuzzyMatcher.values_match?(expected, actual)
end

この FuzzyMatcher.values_match? メソッドが、expectedactual== で比較し、不一致だったら更に matcher.match? で比較するという仕組みになっている。

rspec-support/fuzzy_matcher.rb at cb038ded6e041b86213ecbd34b64fd138bc8ac65 · rspec/rspec-support · GitHub

 def self.values_match?(expected, actual)
   if Hash === actual
     return hashes_match?(expected, actual) if Hash === expected
   elsif Array === expected && Enumerable === actual && !(Struct === actual)
     return arrays_match?(expected, actual.to_a)
   end

   return true if expected == actual

   begin
     expected === actual
   rescue ArgumentError
     # Some objects, like 0-arg lambdas on 1.9+, raise
     # ArgumentError for `expected === actual`.
     false
   end
 end

だから have_attributes にはマッチャもリテラルも両方書ける!

まとめ

たまにはソースコードを読んでみると面白い。