自訂 RSpec Matcher

RSpec 內建的 matchers,比如:have_attributes, eq, match 等等,不僅提供了富表達性的語法,也有更清晰的錯誤訊息。

當我們有一個常用的檢查時,就可以考慮自己定義一個 Matcher,來增加測試的表達性,以及錯誤的可閱讀性。自己寫 Matcher 的壞處就是增加學習成本,除了 RSpec 本身提供的 Matchers 以外,團隊成員都要花心力去學習這個,自己定義的新 Matcher。

接下來看怎麼自己寫一個簡單的 Matcher,比如可以檢查 object instance variables 的 matcher。

沒有用 Matcher 之前:

expect(obj.instance_variable_get("@foo")).to eq "bar"
expect(obj.instance_variable_get("@bar")).to eq "baz"

使用我們自己定義的 matcher:

expect(obj).to have_instance_variables(
  "@foo" => "bar",
  "@bar" => "baz"
)

具體實現:

RSpec::Matchers.define(:have_instance_variables) do |expected|
  match do |actual|
    expected.each do |instance_variable, expected_value|
      expect(actual.instance_variable_get(instance_variable)).to eq(expected_value)
    end
  end

  failure_message do |actual|
    expected.each do |instance_variable, expected_value|
      actual_value = actual.instance_variable_get(instance_variable)
      if actual_value != expected_value
        return "expected #{actual}#{instance_variable} to match #{expected_value.inspect}, but got #{actual_value.inspect}."
      end
    end
  end

  failure_message_when_negated do |actual|
    expected.each do |instance_variable, expected_value|
      actual_value = actual.instance_variable_get(instance_variable)
      if actual_value == expected_value
        return "expected #{actual}#{instance_variable} not to match #{expected_value.inspect}, but got #{actual_value.inspect}."
      end
    end
  end
end
RSpec::Matchers.define(:have_instance_variables)

RSpec 的 to 後面接的便是 matcher 的名字:

expect(obj).to matcher_name(...)

RSpec::Matchers.define 接受的 symbol 便是自定義 Matcher 的名字:

expect(obj).to have_instance_variables(
  "@foo" => "bar",
  "@bar" => "baz"
)
have_instance_variables(
  "@foo" => "bar",
  "@bar" => "baz"
)

其實就是傳入了一個 Hash 給 have_instance_variables{ "@foo" => "bar", "@bar" => "baz" }

而這個 Hash 可以在 RSpec::Matchers.define(:have_instance_variables) do |expected| 區塊的 expected 參數拿到。

匹配的邏輯寫在 match 區塊裡,傳進來的 actual 便是 expect(obj)obj

match do |actual|
  # 匹配的邏輯寫在這裡
end

在這裡我們希望對象的 instance variable 與我們的期望值相等:

expected.each do |instance_variable, expected_value|
  expect(actual.instance_variable_get(instance_variable)).to eq(expected_value)
end

to & not_to 要顯示的錯誤訊息分別定義在 failure_message 以及 failure_message_when_negated 區塊裡。

更多請參考 RSpec 客製化 matcher 的文件:https://www.relishapp.com/rspec/rspec-expectations/v/3-3/docs/custom-matchers/define-matcher

results matching ""

    No results matching ""