メインコンテンツまでスキップ

カスタムマッチャーの定義

rspec-expectationsは、カスタムマッチャーを定義するためのDSLを提供しています。これらは、アプリケーションのドメインでの期待値を表現するのに便利です。

裏側では、RSpec::Matchers.definedefineブロックをシングルトンクラスのコンテキストで評価します。より複雑なマッチャーを作成し、自分自身でClassアプローチを使用したい場合は、弊社のAPIドキュメントに移動して、MatcherProtocolについてのドキュメントを読んでください。

デフォルトのメッセージを持つマッチャーの定義

「matcher_with_default_message_spec.rb」という名前のファイルがあるとします。

require 'rspec/expectations'

RSpec::Matchers.define :be_a_multiple_of do |expected|
match do |actual|
actual % expected == 0
end
end

RSpec.describe 9 do
it { is_expected.to be_a_multiple_of(3) }
end

RSpec.describe 9 do
it { is_expected.not_to be_a_multiple_of(4) }
end

# fail intentionally to generate expected output
RSpec.describe 9 do
it { is_expected.to be_a_multiple_of(4) }
end

# fail intentionally to generate expected output
RSpec.describe 9 do
it { is_expected.not_to be_a_multiple_of(3) }
end

次のコマンドを実行すると:

rspec ./matcher_with_default_message_spec.rb --format documentation

次の結果が得られるはずです:

  • 終了ステータスは0ではないはずです
  • 出力には「is expected to be a multiple of 3」という文言が含まれているはずです
  • 出力には「is expected not to be a multiple of 4」という文言が含まれているはずです
  • 出力には「Failure/Error: it { is_expected.to be_a_multiple_of(4) }」という文言が含まれているはずです
  • 出力には「Failure/Error: it { is_expected.not_to be_a_multiple_of(3) }」という文言が含まれているはずです
  • 出力には「4 examples, 2 failures」という文言が含まれているはずです
  • 出力には「expected 9 to be a multiple of 4」という文言が含まれているはずです
  • 出力には「expected 9 not to be a multiple of 3」という文言が含まれているはずです。

failure_messageのオーバーライド

「matcher_with_failure_message_spec.rb」という名前のファイルがあるとします。

require 'rspec/expectations'

RSpec::Matchers.define :be_a_multiple_of do |expected|
match do |actual|
actual % expected == 0
end
failure_message do |actual|
"expected that #{actual} would be a multiple of #{expected}"
end
end

# fail intentionally to generate expected output
RSpec.describe 9 do
it { is_expected.to be_a_multiple_of(4) }
end

次のコマンドを実行すると:

rspec ./matcher_with_failure_message_spec.rb

次の結果が得られるはずです:

  • 終了ステータスは0ではないはずです
  • 標準出力には「1 example, 1 failure」という文言が含まれているはずです
  • 標準出力には「expected that 9 would be a multiple of 4」という文言が含まれているはずです。

failure_message_when_negatedのオーバーライド

「matcher_with_failure_for_message_spec.rb」という名前のファイルがあるとします。

require 'rspec/expectations'

RSpec::Matchers.define :be_a_multiple_of do |expected|
match do |actual|
actual % expected == 0
end
failure_message_when_negated do |actual|
"expected that #{actual} would not be a multiple of #{expected}"
end
end

# fail intentionally to generate expected output
RSpec.describe 9 do
it { is_expected.not_to be_a_multiple_of(3) }
end

次のコマンドを実行すると:

rspec ./matcher_with_failure_for_message_spec.rb

次の結果が得られるはずです:

  • 終了ステータスは0ではないはずです
  • 標準出力には「1 example, 1 failure」という文言が含まれているはずです。

And 標準出力には "expected that 9 would not be a multiple of 3" が含まれている必要があります。

説明の上書き

Given "matcher_overriding_description_spec.rb" という名前のファイルが次の内容であるとき:

require 'rspec/expectations'

RSpec::Matchers.define :be_a_multiple_of do |expected|
match do |actual|
actual % expected == 0
end
description do
"be multiple of #{expected}"
end
end

RSpec.describe 9 do
it { is_expected.to be_a_multiple_of(3) }
end

RSpec.describe 9 do
it { is_expected.not_to be_a_multiple_of(4) }
end

When rspec ./matcher_overriding_description_spec.rb --format documentation を実行するとき

Then 終了ステータスは 0 である必要があります

And 標準出力には "2 examples, 0 failures" が含まれている必要があります

And 標準出力には "is expected to be multiple of 3" が含まれている必要があります

And 標準出力には "is expected not to be multiple of 4" が含まれている必要があります。

引数なしで

Given "matcher_with_no_args_spec.rb" という名前のファイルが次の内容であるとき:

require 'rspec/expectations'

RSpec::Matchers.define :have_7_fingers do
match do |thing|
thing.fingers.length == 7
end
end

class Thing
def fingers; (1..7).collect {"finger"}; end
end

RSpec.describe Thing do
it { is_expected.to have_7_fingers }
end

When rspec ./matcher_with_no_args_spec.rb --format documentation を実行するとき

Then 終了ステータスは 0 である必要があります

And 標準出力には "1 example, 0 failures" が含まれている必要があります

And 標準出力には "is expected to have 7 fingers" が含まれている必要があります。

複数の引数で

Given "matcher_with_multiple_args_spec.rb" という名前のファイルが次の内容であるとき:

require 'rspec/expectations'

RSpec::Matchers.define :be_the_sum_of do |a,b,c,d|
match do |sum|
a + b + c + d == sum
end
end

RSpec.describe 10 do
it { is_expected.to be_the_sum_of(1,2,3,4) }
end

When rspec ./matcher_with_multiple_args_spec.rb --format documentation を実行するとき

Then 終了ステータスは 0 である必要があります

And 標準出力には "1 example, 0 failures" が含まれている必要があります

And 標準出力には "is expected to be the sum of 1, 2, 3, and 4" が含まれている必要があります。

ブロック引数で

Given "matcher_with_block_arg_spec.rb" という名前のファイルが次の内容であるとき:

require 'rspec/expectations'

RSpec::Matchers.define :be_lazily_equal_to do
match do |obj|
obj == block_arg.call
end

description { "be lazily equal to #{block_arg.call}" }
end

RSpec.describe 10 do
it { is_expected.to be_lazily_equal_to { 10 } }
end

When rspec ./matcher_with_block_arg_spec.rb --format documentation を実行するとき

Then 終了ステータスは 0 である必要があります

And 標準出力には "1 example, 0 failures" が含まれている必要があります

And 標準出力には "is expected to be lazily equal to 10" が含まれている必要があります。

ヘルパーメソッドを使用して

Given "matcher_with_internal_helper_spec.rb" という名前のファイルが次の内容であるとき:

require 'rspec/expectations'

RSpec::Matchers.define :have_same_elements_as do |sample|
match do |actual|
similar?(sample, actual)
end

def similar?(a, b)
a.sort == b.sort
end
end

RSpec.describe "these two arrays" do
specify "should be similar" do
expect([1,2,3]).to have_same_elements_as([2,3,1])
end
end

When rspec ./matcher_with_internal_helper_spec.rb を実行するとき

Then 終了ステータスは 0 である必要があります

And 標準出力には "1 example, 0 failures" が含まれている必要があります。

モジュール内でスコープ化された場合

Given "scoped_matcher_spec.rb" という名前のファイルが次の内容であるとき:

require 'rspec/expectations'

module MyHelpers
extend RSpec::Matchers::DSL

matcher :be_just_like do |expected|
match {|actual| actual == expected}
end
end

RSpec.describe "group with MyHelpers" do
include MyHelpers
it "has access to the defined matcher" do
expect(5).to be_just_like(5)
end
end

RSpec.describe "group without MyHelpers" do
it "does not have access to the defined matcher" do
expect do
expect(5).to be_just_like(5)
end.to raise_exception
end
end

When rspec ./scoped_matcher_spec.rb を実行するとき

その後、stdoutには「2つの例、0つの失敗」と表示される必要があります。

例グループ内でのスコープ

「scoped_matcher_spec.rb」という名前のファイルがあるとき、以下の内容であるとします。

require 'rspec/expectations'

RSpec.describe "group with matcher" do
matcher :be_just_like do |expected|
match {|actual| actual == expected}
end

it "has access to the defined matcher" do
expect(5).to be_just_like(5)
end

describe "nested group" do
it "has access to the defined matcher" do
expect(5).to be_just_like(5)
end
end
end

RSpec.describe "group without matcher" do
it "does not have access to the defined matcher" do
expect do
expect(5).to be_just_like(5)
end.to raise_exception
end
end

rspec scoped_matcher_spec.rbを実行した場合、

出力には「3つの例、0つの失敗」と表示される必要があります。

expect().toとexpect().not_toに対して別々のロジックを持つマッチャー

「matcher_with_separate_should_not_logic_spec.rb」という名前のファイルがあるとき、以下の内容であるとします。

RSpec::Matchers.define :contain do |*expected|
match do |actual|
expected.all? { |e| actual.include?(e) }
end

match_when_negated do |actual|
expected.none? { |e| actual.include?(e) }
end
end

RSpec.describe [1, 2, 3] do
it { is_expected.to contain(1, 2) }
it { is_expected.not_to contain(4, 5, 6) }

# deliberate failures
it { is_expected.to contain(1, 4) }
it { is_expected.not_to contain(1, 4) }
end

rspec matcher_with_separate_should_not_logic_spec.rbを実行した場合、

出力には以下のすべてが含まれる必要があります:

| 4つの例、2つの失敗 | | [1, 2, 3]が1と4を含むことを期待しました | | [1, 2, 3]が1と4を含まないことを期待しました |

マッチャーにアクセス可能なヘルパーメソッドを作成するためにdefine_methodを使用する

「define_method_spec.rb」という名前のファイルがあるとき、以下の内容であるとします。

RSpec::Matchers.define :be_a_multiple_of do |expected|
define_method :is_multiple? do |actual|
actual % expected == 0
end
match { |actual| is_multiple?(actual) }
end

RSpec.describe 9 do
it { is_expected.to be_a_multiple_of(3) }
it { is_expected.not_to be_a_multiple_of(4) }

# deliberate failures
it { is_expected.to be_a_multiple_of(2) }
it { is_expected.not_to be_a_multiple_of(3) }
end

rspec define_method_spec.rbを実行した場合、

出力には以下のすべてが含まれる必要があります:

| 4つの例、2つの失敗 | | 9が2の倍数であることを期待しました | | 9が3の倍数でないことを期待しました |

マッチャーにヘルパーメソッドを持つモジュールを含める

「include_module_spec.rb」という名前のファイルがあるとき、以下の内容であるとします。

module MatcherHelpers
def is_multiple?(actual, expected)
actual % expected == 0
end
end

RSpec::Matchers.define :be_a_multiple_of do |expected|
include MatcherHelpers
match { |actual| is_multiple?(actual, expected) }
end

RSpec.describe 9 do
it { is_expected.to be_a_multiple_of(3) }
it { is_expected.not_to be_a_multiple_of(4) }

# deliberate failures
it { is_expected.to be_a_multiple_of(2) }
it { is_expected.not_to be_a_multiple_of(3) }
end

rspec include_module_spec.rbを実行した場合、

出力には以下のすべてが含まれる必要があります:

| 4つの例、2つの失敗 | | 9が2の倍数であることを期待しました | | 9が3の倍数でないことを期待しました |

値や複合マッチャーを比較するためにvalues_match?を使用する

「compare_values_spec.rb」という名前のファイルがあるとき、以下の内容であるとします。

RSpec::Matchers.define :have_content do |expected|
match do |actual|
# The order of arguments is important for `values_match?`, e.g.
# especially if your matcher should handle `Regexp`-objects
# (`/regex/`): First comes the `expected` value, second the `actual`
# one.
values_match? expected, actual
end
end

RSpec.describe 'a' do
it { is_expected.to have_content 'a' }
end

RSpec.describe 'a' do
it { is_expected.to have_content /a/ }
end

RSpec.describe 'a' do
it { is_expected.to have_content a_string_starting_with('a') }
end

rspec ./compare_values_spec.rb --format documentationを実行した場合、

終了ステータスは0である必要があります。

エラーハンドリング

マッチャーがtrueまたはfalseを返すようにしてください。マッチャー内で適切に例外を処理するように注意してください。たとえば、ほとんどの場合、例外(たとえばArgumentError)が発生した場合にはマッチャーがfalseを返すことを望むかもしれませんが、ユーザーに例外を渡す必要があるエッジケースもあるかもしれません。

各々の StandardError を注意深く扱う必要があります!一括で処理しないでください。

match do |actual|
begin
'[...] Some code'
rescue ArgumentError
false
end
end

次の内容で "error_handling_spec.rb" という名前のファイルがあるとします:

class CustomClass; end

RSpec::Matchers.define :is_lower_than do |expected|
match do |actual|
begin
actual < expected
rescue ArgumentError
false
end
end
end

RSpec.describe 1 do
it { is_expected.to is_lower_than 2 }
end

RSpec.describe 1 do
it { is_expected.not_to is_lower_than 'a' }
end

RSpec.describe CustomClass do
it { expect { is_expected.not_to is_lower_than 2 }.to raise_error NoMethodError }
end

rspec ./error_handling_spec.rb --format documentation を実行すると、

終了ステータスは 0 であるべきです。

マッチャーのエイリアスを定義する

マッチャーを異なるコンテキストで読みやすくするために、.alias_matcher メソッドを使用してマッチャーのエイリアスを提供することができます。

次の内容で "alias_spec.rb" という名前のファイルがあるとします:

RSpec::Matchers.define :be_a_multiple_of do |expected|
match do |actual|
actual % expected == 0
end
end

RSpec::Matchers.alias_matcher :be_n_of , :be_a_multiple_of

RSpec.describe 9 do
it { is_expected.to be_n_of(3) }
end

rspec ./alias_spec.rb --format documentation を実行すると、

終了ステータスは 0 であるべきです。

バブルアップする期待エラーを持つ場合

デフォルトでは、マッチブロックは期待エラー(expect(1).to eq 2 のような期待を使用した場合に発生するエラー)を無視します。これらをバブルアップさせる場合は、オプション :notify_expectation_failures => true を渡してください。

次の内容で "bubbling_expectation_errors_spec.rb" という名前のファイルがあるとします:

RSpec::Matchers.define :be_a_palindrome do
match(:notify_expectation_failures => true) do |actual|
expect(actual).to be_a(String)
expect(actual.reverse).to eq(actual)
end
end

RSpec.describe "a custom matcher that bubbles up expectation errors" do
it "bubbles expectation errors" do
expect("carriage").to be_a_palindrome
end
end

rspec bubbling_expectation_errors_spec.rb を実行すると、

出力には次のすべてが含まれているべきです:

| Failures: | | 1) a custom matcher that bubbles up expectation errors bubbles expectation errors | | Failure/Error: expect(actual.reverse).to eq(actual) | | expected: "carriage" | | got: "egairrac" | | (compared using ==) | | # ./bubbling_expectation_errors_spec.rb:4 | | # ./bubbling_expectation_errors_spec.rb:10 |